【Java并发编程】之三个特性以及happens-before原则

原子性

原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉

可见性

可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。除了sychronized和volatile,final也具有可见性。被final修饰的字段在构造器中一旦初始化完成,并且没有this引用逃逸,那么其他线程就能看到final字段的值。

有序性

sychronized和volatile保证有序性,volatile通过指令重排,sychronized通过同步快。

happens-before规则

在发生操作B之前,操作A产生的影响都能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,它与时间上的先后发生基本没有太大关系。

具体规则:

1.程序次序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作

2.管程锁定规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。同步快中线程安全。

3.volatile变量规则:对一个volatile变量的写操作happens-before对这个变量的读操作。

4.线程启动规则:Thread.start() happens before 所有操作。

5.传递性:如果A happens-before B,且B happens-before C,那么A happens-before C

6.线程终止规则:线程中所有操作都happens-before对此线程的终止检测。

7.对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始

8.程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。

满足任意一个原则,对于读写共享变量来说,就是线程安全

如果一个操作 happens-before 第二个操作,则第一个操作对第二个操作是可见的。

时间上的先后与happens-before

”一个操作时间上先发生于另一个操作“并不代表”一个操作happen—before另一个操作“。

”一个操作happen—before另一个操作“并不代表”一个操作时间上先发生于另一个操作“。

DCL问题

public class LazySingleton {
    private int someField;
    
    private static LazySingleton instance;
    
    private LazySingleton() {
        this.someField = new Random().nextInt(200)+1;         // (1)
    }
    
    public static LazySingleton getInstance() {
        if (instance == null) {                               // (2)
            synchronized(LazySingleton.class) {               // (3)
                if (instance == null) {                       // (4)
                    instance = new LazySingleton();           // (5)
                }
            }
        }
        return instance;                                      // (6)
    }
    
    public int getSomeField() {
        return this.someField;                                // (7)
    }

在volatile的视角审视DCL

instance=new LazySingleton();

这一行代码执行了三个步骤:

 

没有volatile修饰,这些操作可能发生重排序。JVM有可能这样做:

 

  1. 在堆中开辟一块内存(new)
  2. 然后调用对象的构造函数对内存进行初始化(invokespecial)
  3. 最后将引用赋值给变量(astore)
  4. 先在堆中开辟一块内存(new)
  5. 马上将引用赋值给变量(astore)
  6. 最后才是调用对象的构造方法进行初始化(invokespecial)

假设有两条线程:T1、T2,当前时刻T1执行到语句1、T2执行到语句4,有可能会发生下面这个执行时序:

 

 

  1. T2先执行,执行到语句5,但是此时JVM将三条指令进行了重排序:在时间上先执行new、astore、最后才是invokespecial
  2. 执行线程T2的CPU刚刚执行完new、astore指令,还没有来得及执行invokespecial指令就被切换出去了
  3. 线程T1现在登场了,执行if (instance == null),因为线程T2已经执行了astore指令:将引用赋值给了变量,所以该判断语句有可能返回为false。如果返回为false,那么成功拿到对象引用。因为该引用所指向的内存地址还没有进行初始化(执行invokespecial指令),所以只要调用对象的任何方法,就会出错(会不会是NullPointerException?)

 

happens-before分析

这里得到单一的instance实例是没有问题的,问题的关键在于尽管得到了Singleton的正确引用,但是却有可能访问到其成员变量的不正确值。具体来说Singleton.getInstance().getSomeField()有可能返回someField的默认值0。如果程序行为正确的话,这应当是不可能发生的事,因为在构造函数里设置的someField的值不可能为0。为也说明这种情况理论上有可能发生,我们只需要说明语句(1)和语句(7)并不存在happen-before关系。

   假设线程Ⅰ是初次调用getInstance()方法,紧接着线程Ⅱ也调用了getInstance()方法和getSomeField()方法,我们要说明的是线程Ⅰ的语句(1)并不happen-before线程Ⅱ的语句(7)。线程Ⅱ在执行getInstance()方法的语句(2)时,由于对instance的访问并没有处于同步块中,因此线程Ⅱ可能观察到也可能观察不到线程Ⅰ在语句(5)时对instance的写入,也就是说instance的值可能为空也可能为非空。我们先假设instance的值非空,也就观察到了线程Ⅰ对instance的写入,这时线程Ⅱ就会执行语句(6)直接返回这个instance的值,然后对这个instance调用getSomeField()方法,该方法也是在没有任何同步情况被调用,因此整个线程Ⅱ的操作都是在没有同步的情况下调用 ,这时我们便无法利用上述8条happen-before规则得到线程Ⅰ的操作和线程Ⅱ的操作之间的任何有效的happen-before关系(主要考虑规则的第2条,但由于线程Ⅱ没有在进入synchronized块,因此不存在lock与unlock锁的问题),这说明线程Ⅰ的语句(1)和线程Ⅱ的语句(7)之间并不存在happen-before关系,这就意味着线程Ⅱ在执行语句(7)完全有可能观测不到线程Ⅰ在语句(1)处对someFiled写入的值,这就是DCL的问题所在。很荒谬,是吧?DCL原本是为了逃避同步,它达到了这个目的,也正是因为如此,它最终受到惩罚,这样的程序存在严重的bug,虽然这种bug被发现的概率绝对比中彩票的概率还要低得多,而且是转瞬即逝,更可怕的是,即使发生了你也不会想到是DCL所引起的。

    前面我们说了,线程Ⅱ在执行语句(2)时也有可能观察空值,如果是种情况,那么它需要进入同步块,并执行语句(4)。在语句(4)处线程Ⅱ还能够读到instance的空值吗?不可能。这里因为这时对instance的写和读都是发生在同一个锁确定的同步块中,这时读到的数据是最新的数据。为也加深印象,我再用happen-before规则分析一遍。线程Ⅱ在语句(3)处会执行一个lock操作,而线程Ⅰ在语句(5)后会执行一个unlock操作,这两个操作都是针对同一个锁--Singleton.class,因此根据第2条happen-before规则,线程Ⅰ的unlock操作happen-before线程Ⅱ的lock操作,再利用单线程规则,线程Ⅰ的语句(5) -> 线程Ⅰ的unlock操作,线程Ⅱ的lock操作 -> 线程Ⅱ的语句(4),再根据传递规则,就有线程Ⅰ的语句(5) -> 线程Ⅱ的语句(4),也就是说线程Ⅱ在执行语句(4)时能够观测到线程Ⅰ在语句(5)时对Singleton的写入值。接着对返回的instance调用getSomeField()方法时,我们也能得到线程Ⅰ的语句(1) -> 线程Ⅱ的语句(7)(由于线程Ⅱ有进入synchronized块,根据规则2可得),这表明这时getSomeField能够得到正确的值。但是仅仅是这种情况的正确性并不妨碍DCL的不正确性,一个程序的正确性必须在所有的情况下的行为都是正确的,而不能有时正确,有时不正确。


解决DCL

加volatile,禁止指令重排序。

private volatile static LazySingleton instance;

加final

final变量一旦在构造函数中设置完成(前提是在构造函数中没有泄露this引用),其它线程必定会看到在构造函数中设置的值。

参考:https://blog.csdn.net/ns_code/article/details/17348313

 

你可能感兴趣的:(【Java并发编程】之三个特性以及happens-before原则)