Double check lock模式的典型代码:
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
理论上来将,DCL模式能够以优异的性能解决单例模式在多线程下的并发问题,但是实际情况下,因为编译器会采用代码重排(instruction reorder)的优化手段,从而导致偶尔出现返回一个未完全初始化(内存已经分配,reference对象已经创建并且指向创建的内存地址,但是构造函数还没有开始执行或者未完全执行完毕)的Singleton实例并引起程序异常。
为了解决这个问题,JDK1.5以及之后的版本,可以通过volatile以及final关键字来修正DCL中的问题:
volatile比较简单,只需要在instance变量定义前加上volatile关键字即可,由于使用volatile关键字会禁止JVM的代码重排,因此可以解决DCL中存在的问题。
使用final关键字稍微有些地方需要注意,先看代码:
public class FinalWrapper<T> {
public final T value;
public FinalWrapper(T value) {
this.value = value;
}
}
public class Foo {
private FinalWrapper<Helper> helperWrapper = null;
public Helper getHelper() {
FinalWrapper<Helper> wrapper = helperWrapper;
if (wrapper == null) {
synchronized(this) {
if (helperWrapper == null) {
helperWrapper = new FinalWrapper<Helper>(new Helper());
}
wrapper = helperWrapper;
}
}
return wrapper.value;
}
}
首先实例变量value被声明为了final,但是比较特别的一点是在Foo.getHelper()方法中,首先定义了wrapper这个局部变量,并将helperWrapper赋给了它,然后判断wrapper是否为空再进行后续的处理。必须使用wrapper的原因是因为做为局部变量,wrapper不存在线程不安全的问题,如果直接使用helperWrapper,因为之前代码重排的原因,仍然有可能得到一个不完全初始化的对象实例。
再回到final关键字的语义,在JDK1.5以及之后的版本中,编译器能够保证被final关键字定义的属性在对象实例化中能够被正确的初始化(非final对象因为代码重排的原因可能不能满足这个条件),因此在最后一段程序中,wrapper.value永远能够被正确的初始化。
补充1:DCL是在Singleton模式中最容易被使用的,目的是为了解决Singleton在多线程下的线程安全问题。其实为了解决这个问题,除去上述方法之外,还有一些比较直接的方式,例如直接在声明Singleton instance时就将它初始化(无法做到延时加载)或者在getInstance()方法上加入synchronized关键字(性能问题),比较有趣的是下面这种通过内部类来实现延时加载(原理是利用了内部类直到被实际引用时才会加载的机制):
class Foo {
private static class HelperHolder {
public static Helper helper = new Helper();
}
public static Helper getHelper() {
return HelperHolder.helper;
}
}
补充2:关于volatile关键字,volatile变量保证在各个工作线程内存之间的一致性,但是不代表任何情况下基于volatile变量的操作都是线程安全的(在操作依赖于当前变量值并且会修改它的情况下,因此我们仍然需要使用DCL来保证单例的唯一性)。同时volatile变量保证了代码顺序的不变性,但是在和其他非volatile变量一起使用时,仍然会触发编译器的代码重排。