欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

单例与双重检测

程序员文章站 2022-07-14 09:51:42
...
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的,即赋值语句在对象初始化之前调用,此时别的线程得的是一个还未初始化的对象,这样会导致系统崩溃。

  1. 线程Ⅰ进入getInstance()方法。
  2. 由于instance为null,线程Ⅰ在(1)处进入synchronized块。
  3. 线程Ⅰ进行到(3)处,但在构造函数执行之前,使用实例成为非null。
  4. 线程Ⅰ被线程Ⅱ预占。
  5. 线程Ⅱ检查实例是否为null。因为实例不为null,线程Ⅱ将instance引用返回给一个构造完整但部分初始化了的Singleton对象。
  6. 线程Ⅱ被线程1预占。
  7. 线程Ⅰ通过运行Singleton对象的构造函数并将引用返回给它,来完成对该对象的初始化。

为展示事件的发生情况,假设代码行instance = new Singleton();执行了下列伪代码

mem = allocate();           	//为单例对象分配内存空间
instance = mem;              	//注意,instance引用现在是非空状态,但还未初始化
ctorSingleton(instance);    	//为单例对象通过instance调用构造函数

关于双重检测的另一些思考

  1. 必须对同一个变量的所有读写同步,才能保证不读取到陈旧的数据,仅仅同步读或写是不够的。
  2. 尽管得到了LazySingleton的正确引用,但是却有可能访问到其成员变量的不正确值
  3. 对引用(包括对象引用和数组引用)的非同步访问,即使得到该引用的最新值,却并不能保证也能得到其成员变量(对数组而言就是每个数组元素)的最新值。
  4. 即使对于不可变对象,它也必须被安全的发布,才能被安全地共享。

栗子:

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中就能够正确运行了。

相关标签: 单例模式