java并发编程:线程同步和锁

一、锁的原理

java中每个对象都有一个内置锁。当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this)有关的锁。获得一个对象的锁也称为获取锁,当程序运行到synchronized同步方法或代码块时该对象的锁才起作用。

一个对象只有一个锁。所以,只能被一个线程获取,其他线程要想获取锁,必须等到这个线程释放锁。就是意味着其他线程不能进入该对象上的synchronized方法或代码块,直到锁被释放。释放锁就是指持有锁的线程退出了synchronized方法或代码块。

关于锁和同步,有几个要点:

  • 只能同步方法,而不能同步变量和类;
  • 每个对象只有一个锁;当提到同步时,应该清楚再什么上同步?也就是说,在那个对象上同步
  • 不比同步类中所有的方法,类可以同时拥有同步和非同步方法
  • 如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需等待,直到锁被释放。也就是说:如果一个线程在对象上获取一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法
  • 如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制
  • 线程睡眠时,它所持的任何锁都不会释放
  • 线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁
  • 同步损害并发性,应尽可能的缩小同步的范围。尽量使用同步代码块
  • 在使用同步代码块的时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁

1.1、静态方法同步

静态优先于对象加载,所以静态方法的同步锁就是这个对象的class

1.2、如果线程不能获取锁会怎么样

如果线程要进入同步方法,而其锁已被占用,则线程在该对象上被阻塞。实际上,线程进入该对象的一种池中,必须在那里等待,知道锁被释放,该线程再次变为可运行

当考虑阻塞时,一定要搞清楚持有哪个对象锁

  • 调用同一个对象中非静态同步方法的线程将彼此阻塞。如果是不同对象,则每个线程有自己的对象锁,线程之间互不干预
  • 调用同一个类中的静态同步方法的线程将彼此阻塞,他们的对象锁相同,为当前class
  • 静态同步方法和非静态同步方法之间不会彼此阻塞,因为静态锁是class,非静态的是当前对象
  • 对于同步代码块,要看清楚对象锁。在同一个对象上进行同步的线程将彼此阻塞,在不同对象上锁定的线程将永不会彼此阻塞

1.3、什么时候需要同步

在多个线程同时访问并需要更改数据时,应该同步以保护数据,确保两个线程不会同时修改它

1.4、线程安全类

当一个类用了同步措施来保护数据时,这个类就是“线程安全”的。即使是线程安全类,也应该特别小心

public class NameList { 
    private List nameList = Collections.synchronizedList(new LinkedList()); 

    public void add(String name) { 
        nameList.add(name); 
    } 

    public String removeFirst() { 
        if (nameList.size() > 0) { 
            return (String) nameList.remove(0); 
        } else { 
            return null; 
        } 
    } 
}
public class Test { 
    public static void main(String[] args) { 
        final NameList nl = new NameList(); 
        nl.add("aaa"); 
        class NameDropper extends Thread{ 
            public void run(){ 
                String name = nl.removeFirst(); 
                System.out.println(name); 
            } 
        } 

        Thread t1 = new NameDropper(); 
        Thread t2 = new NameDropper(); 
        t1.start(); 
        t2.start(); 
    } 
}

虽然集合对象private List nameList = Collections.synchronizedList(new LinkedList());是同步的,但是程序还不是线程安全的。出现这种事件的原因是,上例中一个线程操作列表过程中无法阻止另外一个线程对列表的其他操作。解决上面问题的办法是,在操作集合对象的NameList上面做一个同步。改写后的代码如下:

public class NameList { 
    private List nameList = Collections.synchronizedList(new LinkedList()); 

    public synchronized void add(String name) { 
        nameList.add(name); 
    } 

    public synchronized String removeFirst() { 
        if (nameList.size() > 0) { 
            return (String) nameList.remove(0); 
        } else { 
            return null; 
        } 
    } 
}

 

1.5、线程死锁

当两个线程都被阻塞,每个线程都在等待获取另一个线程的锁时就发生了死锁

public class DeadlockRisk { 
    private static class Resource { 
        public int value; 
    } 

    private Resource resourceA = new Resource(); 
    private Resource resourceB = new Resource(); 

    public int read() { 
        synchronized (resourceA) { 
            synchronized (resourceB) { 
                return resourceB.value + resourceA.value; 
            } 
        } 
    } 

    public void write(int a, int b) { 
        synchronized (resourceB) { 
            synchronized (resourceA) { 
                resourceA.value = a; 
                resourceB.value = b; 
            } 
        } 
    } 
}

死锁的根本原因就是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环;

避免的方法就是避免同步的嵌套使用,指定获取锁的顺序

二、总结

1、线程同步的目的是为了保护多个线程访问一个资源造成数据的不确定性

2、同步是同过锁来实现,每个对象有且仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他同步方法

3、对于静态同步方法,锁是这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象的锁

4、对于同步,要时刻清醒在哪个对象上同步,这是关键

5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和完全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源

6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞

7、死锁是线程间相互等待锁而造成的

 

posted @ 2019-02-27 16:18  hy_wx  阅读(234)  评论(0编辑  收藏  举报