指令重排序和Happen-Before
Happen-Before指的是两个操作之前的一种偏序关系,比如,操作A happen-before 操作B,那么操作A对于操作B是可见的(即操作A产生的影响会作用于操作B,如共享变量更新,消息发送,方法调用等),记作hb(A, B)。比如,下面的代码:
1. i = 1; // T12. j = i; // T23. i = 2; // T3 操作1, 2, 3分别在线程T1, T2, T3中执行,若有偏序关系hb(1, 2),即T1的操作1对T2的操作2是可见的,那么,T2的操作2执行后,变量j的值一定为1(此时T3的操作3还未执行),但若T3的操作3发生在操作1,2之间,就不能确定最终i的值是多少,因为操作3,2并没有偏序关系,因此操作3的结果有可能影响操作2,也有可能不会。
JMM中的happen-before
JMM中已经规范出一些典型的hb关系,摘自周志明的<<深入理解Java虚拟机>>331页,也可查看官方JLS:1. 程序次序规则: 同一线程内,前面的动作hb后面的动作。
2. 管程锁定规则: 对于同一个锁,unlock()操作hb下一次lock()操作。
3. volatile变量规则: volatile变量的写操作hb之后的读操作。
4. 线程启动规则: 同一线程的start()方法hb其他方法。
5. 线程终止规则: 同一线程的任何方法都hb对此线程的终止检测。如Thread.join(),Thread.isAlive()等。
6. 线程中断规则: 对线程interrupt()方法的调用hb发生于被中断线程的代码检测到中断事件发生。
7. 对象终结规则: 一个对象的初始化完成hb该对象的finalize()方法。
8. 传递性: A操作hb操作B,操作Bhb操作C,那么A操作hb操作C。
DCL(Double Checking Locked)问题
对于hb关系的理解,DCL算是一个比较典型的问题,它被广泛运用于多线程环境中的懒加载,但在没额外同步约束下,是不能可靠地运行的。如下面的代码片段:class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) helper = new Helper(); return helper; }} 在多线程环境中,上面的代码有可能会实例化两个Helper实例,因此需要必要的同步操作:class Foo { private Helper helper = null; public synchronized Helper getHelper() { if (helper == null) helper = new Helper(); return helper; }} 上面的代码在每次getHelper()都进行加锁,Double-Checked-Locking则尝试去掉这样的加锁方式:class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) synchronized(this) { if (helper == null) helper = new Helper(); } return helper; }} 然而,这段代码在优化型编译器或共享的内存处理器上可能不会正常工作。为何上面的代码不能正常工作?
上面的代码不能正常工作的原因很多,有一些比较明显,而有一次可能就比较难发现。第一个原因
最明显的原因是Helper对象的构造函数执行操作和helper字段的赋值操作有可能被重排序。因此,调用getHelper()方法的线程虽然看到helper对象为非null,但仅看到了helper对象各字段的默认值,而没有看到构造函数执行后的helper对象。
如果编译器内联地调用构造函数,只要构造函数不抛出异常或者进行同步操作,Helper对象的构造函数执行操作和helper字段的赋值操作就可以自由排序,即使编译器不排序这些写操作,在多处理器系统中也有可能进行排序。
比如,在赛门铁克(名字略感亲切)的JIT编译器(使用基于句柄的对象分配系统)中,下面的代码就不能正常工作:singletons[i].reference = new Singleton(); 编译后的指令为:0206106A mov eax,0F97E78h0206106F call 01F6B210 ; allocate space for ; Singleton, return result in eax02061074 mov dword ptr [ebp],eax ; EBP is &singletons[i].reference ; store the unconstructed object here.02061077 mov ecx,dword ptr [eax] ; dereference the handle to ; get the raw pointer02061079 mov dword ptr [ecx],100h ; Next 4 lines are0206107F mov dword ptr [ecx+4],200h ; Singleton's inlined constructor02061086 mov dword ptr [ecx+8],400h0206108D mov dword ptr [ecx+0Ch],0F84030h 可见,singletons[i].reference的分配动作发生在Singleton构造函数调用之前。针对上述问题,进行一些修复后:class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) { Helper h; synchronized(this) { h = helper; if (h == null) { synchronized (this) { h = new Helper(); } // monitor被释放 helper = h; }