java happen before

下面是Java内存模型中的八条可保证happen—before的规则java

一、程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操做happen—before(时间上)后执行的操做。数组

    二、管理锁定规则:一个unlock操做happen—before后面(时间上的前后顺序,下同)对同一个锁的lock操做。安全

    三、volatile变量规则:对一个volatile变量的写操做happen—before后面对该变量的读操做。多线程

    四、线程启动规则:Thread对象的start()方法happen—before此线程的每个动做。app

    五、线程终止规则:线程的全部操做都happen—before对此线程的终止检测,能够经过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。dom

    六、线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。函数

    七、对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。this

    八、传递性:若是操做A happen—before操做B,操做B happen—before操做C,那么能够得出A happen—before操做C。线程

 

”一个操做时间上先发生于另外一个操做“并不表明”一个操做happen—before另外一个操做“。对象

解决方法:能够将setValue(int)方法和getValue()方法均定义为synchronized方法,也能够把value定义为volatile变量(value的修改并不依赖value的原值,符合volatile的使用场景),分别对应happen—before规则的第2和第3条。注意,只将setValue(int)方法和getvalue()方法中的一个定义为synchronized方法是不行的,必须对同一个变量的全部读写同步,才能保证不读取到陈旧的数据,仅仅同步读或写是不够的 。

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

 

DCL即双重检查加锁

public class LazySingleton {
    private int someFiled;
    private static LazySingleton instance;

    private LazySingleton() {
        this.someFiled = new Random().nextInt(200) + 1; // 1
    }

    // 不能彻底保证多线程场景下someField值的同步
    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 getSomeFiled() {
        return this.someFiled;                          // 7
    }
}

这里获得单一的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的分析也告诉咱们一条经验原则:对引用(包括对象引用和数组引用)的非同步访问,即便获得该引用的最新值,却并不能保证也能获得其成员变量(对数组而言就是每一个数组元素)的最新值。

解决方案:

    一、最简单并且安全的解决方法是使用static内部类的思想,它利用的思想是:一个类直到被使用时才被初始化,而类初始化的过程是非并行的,这些都有JLS保证。

以下述代码:

public class Singleton {
    private Singleton() {
    }

    private static class InstanceHolder {
        private static final Singleton instance = new Singleton();
    }

    private static Singleton getInstance() {
        return InstanceHolder.instance;
    }
}

二、另外,能够将instance声明为volatile,即

private volatile static LazySingleton instance; 

    这样咱们即可以获得,线程Ⅰ的语句(5) -> 语线程Ⅱ的句(2),根据单线程规则,线程Ⅰ的语句(1) -> 线程Ⅰ的语句(5)和线程Ⅱ的语句(2) -> 线程Ⅱ的语句(7),再根据传递规则就有线程Ⅰ的语句(1) -> 线程Ⅱ的语句(7),这表示线程Ⅱ可以观察到线程Ⅰ在语句(1)时对someFiled的写入值,程序可以获得正确的行为。

   注:

    一、volatile屏蔽指令重排序的语义在JDK1.5中才被彻底修复,此前的JDK中即便将变量声明为volatile,也仍然不能彻底避免重排序所致使的问题(主要是volatile变量先后的代码仍然存在重排序问题),这点也是在JDK1.5以前的Java中没法安全使用DCL来实现单例模式的缘由。

    二、把volatile写和volatile读这两个操做综合起来看,在读线程B读一个volatile变量后,写线程A在写这个volatile变量以前,全部可见的共享变量的值都将当即变得对读线程B可见。

   三、 在java5以前对final字段的同步语义和其它变量没有什么区别,在java5中,final变量一旦在构造函数中设置完成(前提是在构造函数中没有泄露this引用),其它线程一定会看到在构造函数中设置的值。而DCL的问题正好在于看到对象的成员变量的默认值,所以咱们能够将LazySingleton的someField变量设置成final,这样在java5中就可以正确运行了。

相关文章
相关标签/搜索