单例与双重检测
public class Singleton {
private volatile static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) { // (1)
if (instance == null) { // (2)
instance = new Singleton(); // (3)
}
}
}
return instance;
}
}
单例与双重检测
双重检测锁定失败的问题并不归咎于JVM中的实现bug,而是归咎于Java平台内存模型。内存模型允许所谓的“无序重写”,这也是失败的一个主要原因。
无序写入:
这行代码的问题是:在Singleton构造体执行之前,变量instance可能成为非null的,即赋值语句在对象初始化之前调用,此时别的线程得的是一个还未初始化的对象,这样会导致系统崩溃。
- 线程Ⅰ进入getInstance()方法。
- 由于instance为null,线程Ⅰ在(1)处进入synchronized块。
- 线程Ⅰ进行到(3)处,但在构造函数执行之前,使用实例成为非null。
- 线程Ⅰ被线程Ⅱ预占。
- 线程Ⅱ检查实例是否为null。因为实例不为null,线程Ⅱ将instance引用返回给一个构造完整但部分初始化了的Singleton对象。
- 线程Ⅱ被线程1预占。
- 线程Ⅰ通过运行Singleton对象的构造函数并将引用返回给它,来完成对该对象的初始化。
为展示事件的发生情况,假设代码行instance = new Singleton();执行了下列伪代码
mem = allocate(); //为单例对象分配内存空间
instance = mem; //注意,instance引用现在是非空状态,但还未初始化
ctorSingleton(instance); //为单例对象通过instance调用构造函数
- 必须对同一个变量的所有读写同步,才能保证不读取到陈旧的数据,仅仅同步读或写是不够的。
- 尽管得到了LazySingleton的正确引用,但是却有可能访问到其成员变量的不正确值
- 对引用(包括对象引用和数组引用)的非同步访问,即使得到该引用的最新值,却并不能保证也能得到其成员变量(对数组而言就是每个数组元素)的最新值。
- 即使对于不可变对象,它也必须被安全的发布,才能被安全地共享。
栗子:
public class LazySingleton {
private int someField;
private static LazySingleton instance;
private LazySingleton() {
this.someField = new Random().nextInt(200)+1; // (1)
}
public static LazySingleton getInstance() {
if (instance == null) { // (2)
synchronized(LazySingleton.class) { // (3)
if (instance == null) { // (4)
instance = new LazySingleton(); // (5)
}
}
}
return instance; // (6)
}
public int getSomeField() {
return this.someField; // (7)
}
}
LazySingleton.getInstance().getSomeField()有可能返回someField的默认值0。我们只需要说明语句(1)和语句(7)并不存在happen-before关系。
假设线程Ⅰ是初次调用getInstance()方法,紧接着线程Ⅱ也调用了getInstance()方法和getSomeField()方法,我们要说明的是线程Ⅰ的语句(1)并不happen-before线程Ⅱ的语句(7)。
线程Ⅱ在执行getInstance()方法的语句(2)时,由于对instance的访问并没有处于同步块中,因此线程Ⅱ可能观察到也可能观察不到线程Ⅰ在语句(5)时对instance的写入,也就是说instance的值可能为空也可能为非空。
我们先假设instance的值非空,也就观察到了线程Ⅰ对instance的写入,这时线程Ⅱ就会执行语句(6)直接返回这个instance的值,然后对这个instance调用getSomeField()方法,该方法也是在没有任何同步情况被调用,因此整个线程Ⅱ的操作都是在没有同步的情况下调用。
这时我们无法利用第1条和第2条happen-before规则得到线程Ⅰ的操作和线程Ⅱ的操作之间的任何有效的happen-before关系,这说明线程Ⅰ的语句(1)和线程Ⅱ的语句(7)之间并不存在happen-before关系,这就意味着线程Ⅱ在执行语句(7)完全有可能观测不到线程Ⅰ在语句(1)处对someFiled写入的值,
这就是DCL的问题所在。
在Java5或以后,将someField声明成final的,即使它不被安全的发布,也能被安全地共享,而在Java1.4或以前则必须被安全地发布。
步入Java5,在java 5中多增加了一条happen-before规则:
对volatile字段的写操作happen-before后续的对同一个字段的读操作。
利用这条规则我们可以将instance声明为volatile,即:
private volatile static LazySingleton instance;
根据这条规则,我们可以得到,线程Ⅰ的语句(5) -> 线程Ⅱ的语句(2),根据单线程规则,线程Ⅰ的语句(1) -> 线程Ⅰ的语句(5)和线程Ⅱ的语句(2) -> 线程Ⅱ的语句(7),再根据传递规则就有线程Ⅰ的语句(1) -> 线程Ⅱ的语句(7),这表示线程Ⅱ能够观察到线程Ⅰ在语句(1)时对someFiled的写入值,程序能够得到正确的行为。
在java5之前对final字段的同步语义和其它变量没有什么区别,在java5中,final变量一旦在构造函数中设置完成(前提是在构造函数中没有泄露this引用),其它线程必定会看到在构造函数中设置的值。而DCL的问题正好在于看到对象的成员变量的默认值,因此我们可以将LazySingleton的someField变量设置成final,这样在java5中就能够正确运行了。
上一篇: 单例模式与双重检测
下一篇: 双重检测锁实现单例模式