https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4
Happens-before 的定义
通过上面的定义,我们得出 happens-before 的原则:
happens-before 定义了什么时候会发生数据争用(即:多线程读取共享变量)。
happens-before 原则:
1. 对锁(对象监视器)的 释放(unlock)操作 happens-before 锁的 lock 操作
2. 对 volatile 字段的写操作 happens-before 对这个字段的读操作
从底层实现来看,volatile 写操作会通过汇编中的 lock 前缀指令,对这块内存区域的缓存行进行锁定(相当于加入了内存屏障,从而保证程序执行顺序),
这样,如果此时正在对 volatile 变量时行写操作,那么其他线程所有的读操作都需要等待写操作完成,这就是 volatile 写操作 happens-before 对这个字段读操作的原因
3. 对线程的 start() 的调用 happens-before 这个线程里面的代码的执行
4. t1.join() 成功后,那么 t1 线程里面的代码的执行 happens-before main 线程
5. 一个对象默认的初始化操作 happens-before 这个程序里面的其他操作
当一个程序包含两个冲突的访问,且这两个访问没有按照 happens-before 原则进行排序,那么,我们称之为数据争用。
线程间操作以外的操作的语义,例如,读取数组长度、执行已检查的强制转换,以及调用虚拟方法,不会直接影响数据争用。
当且仅当所有顺序一致的执行都没有数据争用时,程序才能正确同步。
如果一个程序正确同步,则该程序的所有执行将看起来是顺序一致的。
这是对程序员的极其有力的保证。程序员无需考虑重排序(指令重排)即可确定其代码包含数据争用。因此,在确定其代码是否正确同步时,他们无需考虑指令重排。一旦确定代码正确同步,程序员就不必担心指令重排会影响他的代码。
一个程序必须正确的同步,从而避免在指令重排时得到意想不到的结果。使用正确的同步不能确保程序的整体行为正确。但是,同步的使用让程序员可以以一种简单的方式来推理程序的可能行为。正确同步的程序的行为很少依赖于可能的指令重排。没有正确同步的程序,就可能得到非常奇怪、令人困惑和违反直觉的结果。
volatile 的应用之——双重校验锁实现单例模式:
这种方式必须使用 volatile 修饰符,以确保程序的正确性,避免指令重排导致获取到一个未被初始化的对象,从而导致程序崩溃。这里使用到了 volatile 可以保证有序性的特性
(volatile 是 jdk 1.5 之后出现的)
1 /** 2 * 查看字节码 3 * javap -v -p -s -sysinfo -constants NewTest.class 4 * javap -v NewTest.class 5 */ 6 public class NewTest { 7 public volatile Object o; 8 9 /** 10 * <pre> 11 * public void m(); 12 * descriptor: ()V 13 * flags: ACC_PUBLIC 14 * Code: 15 * stack=3, locals=1, args_size=1 16 * 0: aload_0 17 * 1: new #2 // class java/lang/Object 18 * 4: dup 19 * 5: invokespecial #1 // Method java/lang/Object."<init>":()V 20 * 8: putfield #3 // Field o:Ljava/lang/Object; 21 * 11: return 22 * </pre> 23 */ 24 public void m(){ 25 // 1. 分配内存 26 // 2. 调用 Object 的构造函数,初始化对象 27 // 3. 将对象内存的引用赋值给变量 o 28 o = new Object(); 29 } 30 }
参考:
https://en.wikipedia.org/wiki/Double-checked_locking
https://javarevisited.blogspot.com/2014/05/double-checked-locking-on-singleton-in-java.html