单例与双重检测

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值