指令重排序和Happen-Before

JerryXia 发表于 , 阅读 (0)
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;                }