Java语言中有一个“先行发生”(happen—before)的规则,它是Java内存模型中定义的两项操做之间的偏序关系,若是操做A先行发生于操做B,其意思就是说,在发生操做B以前,操做A产生的影响都能被操做B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,它与时间上的前后发生基本没有太大关系。这个原则特别重要,它是判断数据是否存在竞争、线程是否安全的主要依据。
java
举例来讲,假设存在以下三个线程,分别执行对应的操做:web
线程A中执行以下操做:i=1
线程B中执行以下操做:j=i
线程C中执行以下操做:i=2
假设线程A中的操做”i=1“ happen—before线程B中的操做“j=i”,那么就能够保证在线程B的操做执行后,变量j的值必定为1,即线程B观察到了线程A中操做“i=1”所产生的影响;如今,咱们依然保持线程A和线程B之间的happen—before关系,同时线程C出如今了线程A和线程B的操做之间,可是C与B并无happen—before关系,那么j的值就不肯定了,线程C对变量i的影响可能会被线程B观察到,也可能不会,这时线程B就存在读取到不是最新数据的风险,不具有线程安全性。数组
下面是Java内存模型中的八条可保证happen—before的规则,它们无需任何同步器协助就已经存在,能够在编码中直接使用。若是两个操做之间的关系不在此列,而且没法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机能够对它们进行随机地重排序。
缓存
一、程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操做happen—before(时间上)后执行的操做。安全
二、管理锁定规则:一个unlock操做happen—before后面(时间上的前后顺序,下同)对同一个锁的lock操做。微信
三、volatile变量规则:对一个volatile变量的写操做happen—before后面对该变量的读操做。app
四、线程启动规则:Thread对象的start()方法happen—before此线程的每个动做。dom
五、线程终止规则:线程的全部操做都happen—before对此线程的终止检测,能够经过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。ide
六、线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。函数
七、对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
八、传递性:若是操做A happen—before操做B,操做B happen—before操做C,那么能够得出A happen—before操做C。
”时间上执行的前后顺序“与”happen—before“之间有何不一样呢?
一、首先来看操做A在时间上先与操做B发生,是否意味着操做A happen—before操做B?
一个经常使用来分析的例子以下:
private int value = 0;
public int get(){
return value;
}
public void set(int value){
this.value = value;
}
假设存在线程A和线程B,线程A先(时间上的先)调用了setValue(3)操做,而后(时间上的后)线程B调用了同一对象的getValue()方法,那么线程B获得的返回值必定是3吗?
对照以上八条happen—before规则,发现没有一条规则适合于这里的value变量,从而咱们能够断定线程A中的setValue(3)操做与线程B中的getValue()操做不存在happen—before关系。所以,尽管线程A的setValue(3)在操做时间上先于操做B的getvalue(),但没法保证线程B的getValue()操做必定观察到了线程A的setValue(3)操做所产生的结果,也便是getValue()的返回值不必定为3(有多是以前setValue所设置的值)。这里的操做不是线程安全的。
所以,”一个操做时间上先发生于另外一个操做“并不表明”一个操做happen—before另外一个操做“。
解决方法:能够将setValue(int)方法和getValue()方法均定义为synchronized方法,也能够把value定义为volatile变量(value的修改并不依赖value的原值,符合volatile的使用场景),分别对应happen—before规则的第2和第3条。注意,只将setValue(int)方法和getvalue()方法中的一个定义为synchronized方法是不行的,必须对同一个变量的全部读写同步,才能保证不读取到陈旧的数据,仅仅同步读或写是不够的 。
二、其次来看,操做A happen—before操做B,是否意味着操做A在时间上先与操做B发生?
看有以下代码:
x = 1;
y = 2;
假设同一个线程执行上面两个操做:操做A:x=1和操做B:y=2。根据happen—before规则的第1条,操做A happen—before 操做B,可是因为编译器的指令重排序(Java语言规范规定了JVM线程内部维持顺序化语义,也就是说只要程序的最终结果等同于它在严格的顺序化环境下的结果,那么指令的执行顺序就可能与代码的顺序不一致。这个过程经过叫作指令的重排序。指令重排序存在的意义在于:JVM可以根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的从新排序机器指令,使机器指令更符合CPU的执行特色,最大限度的发挥机器的性能。在没有同步的状况下,编译器、处理器以及运行时等均可能对操做的执行顺序进行一些意想不到的调整)等缘由,操做A在时间上有可能后于操做B被处理器执行,但这并不影响happen—before原则的正确性。
所以,”一个操做happen—before另外一个操做“并不表明”一个操做时间上先发生于另外一个操做“。
最后,一个操做和另外一个操做一定存在某个顺序,要么一个操做或者是先于或者是后于另外一个操做,或者与两个操做同时发生。同时发生是彻底可能存在的,特别是在多CPU的状况下。而两个操做之间却可能没有happen-before关系,也就是说有可能发生这样的状况,操做A不happen-before操做B,操做B也不happen-before操做A,用数学上的术语happen-before关系是个偏序关系。两个存在happen-before关系的操做不可能同时发生,一个操做A happen-before操做B,它们一定在时间上是彻底错开的,这实际上也是同步的语义之一(独占访问)。
DCL即双重检查加锁。下面是一个典型的在单例模式中使用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)
}
}
这里获得单一的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() {}
// Lazy initialization holder class idiom for static fields
private static class InstanceHolder {
private static final Singleton instance = new Singleton();
}
public static Singleton getSingleton() {
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中就可以正确运行了。
来源:https://blog.csdn.net/ns_code/article/details/17348313