原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。
可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。除了sychronized和volatile,final也具有可见性。被final修饰的字段在构造器中一旦初始化完成,并且没有this引用逃逸,那么其他线程就能看到final字段的值。
sychronized和volatile保证有序性,volatile通过指令重排,sychronized通过同步快。
在发生操作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 第二个操作,则第一个操作对第二个操作是可见的。
”一个操作时间上先发生于另一个操作“并不代表”一个操作happen—before另一个操作“。
”一个操作happen—before另一个操作“并不代表”一个操作时间上先发生于另一个操作“。
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=new LazySingleton();
这一行代码执行了三个步骤:
没有volatile修饰,这些操作可能发生重排序。JVM有可能这样做:
假设有两条线程:T1、T2,当前时刻T1执行到语句1、T2执行到语句4,有可能会发生下面这个执行时序:
if (instance == null)
,因为线程T2已经执行了astore指令:将引用赋值给了变量,所以该判断语句有可能返回为false。如果返回为false,那么成功拿到对象引用。因为该引用所指向的内存地址还没有进行初始化(执行invokespecial指令),所以只要调用对象的任何方法,就会出错(会不会是NullPointerException?)
这里得到单一的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的不正确性,一个程序的正确性必须在所有的情况下的行为都是正确的,而不能有时正确,有时不正确。
加volatile,禁止指令重排序。
private volatile static LazySingleton instance;
加final
final变量一旦在构造函数中设置完成(前提是在构造函数中没有泄露this引用),其它线程必定会看到在构造函数中设置的值。
参考:https://blog.csdn.net/ns_code/article/details/17348313