Java基础教程:多线程杂谈——双重检查锁与Volatile
双重检查锁
有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化。此时程序员可能会采用延迟初始化。但要正确实现线程安全的延迟初始化需要一些技巧,否则很容易出现问题。比如,下面是非线程安全的延迟初始化对象的示例代码:
public class A{ private Instance instance; public Instance getInstance(){ if(instance==null){ instance = new Instance(); } } return instance; } }
因为在多线程执行下,instance可能被多次初始化。我们可以对 getInstance() 做同步处理来实现线程安全的延迟初始化。示例代码如下:
public class A{ private Instance instance; public Instance getInstance(){ if(instance==null){ synchronized(A.class){ if(instance==null) instance = new Instance(); } } return instance; } }
可见,我们用了两次is null 判断,如上则是通过双重检查锁定来降低同步的开销。
问题
前面的双重检查锁定示例代码(instance = new Singleton();)创建一个对象。这一行代码可以分解为如下的三行伪代码:
memory = allocate(); //1:分配对象的内存空间 ctorInstance(memory); //2:初始化对象 instance = memory; //3:设置 instance 指向刚分配的内存地址
上面三行伪代码中的 2 和 3 之间,可能会被重排序(在一些 JIT 编译器上,这种重排序是真实发生的,详情见参考文献 1 的“Out-of-order writes”部分)。2 和 3 之间重排序之后的执行时序如下:
memory = allocate(); //1:分配对象的内存空间 instance = memory; //3:设置 instance 指向刚分配的内存地址 // 注意,此时对象还没有被初始化!ctorInstance(memory); //2:初始化对象
所以说可能出现一种情况就是,锁内线程创建对象时仅仅分配了内存地址还未完成初始化,锁外的线程就会判断is null 为false,从而返回一个半成品实例。问题的关键所在就是创建对象并非原子操作,从而被编译器进行指令重排序,导致发生意想不到为问题。
解决办法
为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量,volatile的禁止重排序保证了操作的有序性。