`
freish
  • 浏览: 82907 次
  • 性别: Icon_minigender_1
  • 来自: 摄影帝国
社区版块
存档分类
最新评论

双重检查锁定失败可能性——参照《The "Double-Checked Locking is Broken" Declaration》

    博客分类:
  • java
阅读更多

 

双重检查锁定在延迟初始化的单例模式中见得比较多(单例模式实现方式很多,这里为说明双重检查锁定问题,只选取这一种方式),先来看一个版本:

 

public class Singleton {

    private static Singleton instance = null;

    private Singleton(){}

   

    public static Singleton  getInstance() {

       if(instance == null) {

           instance = new Singleton();

       }

       return instance;

    }

}

上面是最原始的模式,一眼就可以看出,在多线程环境下,可能会产生多个Singleton实例,于是有了其同步的版本:

public class Singleton {

    private static Singleton instance = null;

    private Singleton(){}

   

    public synchronized static Singleton getInstance() {

       if(instance == null) {

           instance = new Singleton();

       }

       return instance;

    }

}

在这个版本中,每次调用getInstance都需要取得Singleton.class上的锁,然而该锁只是在开始构建Singleton 对象的时候才是必要的,后续的多线程访问,效率会降低,于是有了接下来的版本:

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;

    }

}

很好的想法!不幸的是,该方案也未能解决问题之根本:

 

原因在于:初始化Singleton   将对象地址写到instance字段 的顺序是不确定的。在某个线程new Singleton()时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化;此时若另外一个线程来调用getInstance,取到的就是状态不正确的对象。

 

鉴于以上原因,有人可能提出下列解决方案:

public class Singleton {

    private static Singleton instance = null;

    private Singleton(){}

   

    public static Singleton getInstance() {

       if(instance == null) {

           Singleton temp;

           synchronized(Singleton.class) {

              temp = instance;

              if(temp == null) {

                  synchronized(Singleton.class) {

                     temp = new Singleton();

                  }

                  instance = temp;

              }

           }

       }

       return instance;

    }

}

该方案将Singleton对象的构造置于最里面的同步块,这种思想是在退出该同步块时设置一个内存屏障,以阻止初始化Singleton   将对象地址写到instance字段 的重新排序。

 

不幸的是,这种想法也是错误的,同步的规则不是这样的。退出监视器(退出同步)的规则是:所以在退出监视器前面的动作都必须在释放监视器之前完成。然而,并没有规定说退出监视器之后的动作不能放到退出监视器之前完成。也就是说同步块里的代码必须在退出同步时完成,而同步块后面的代码则可以被编译器或运行时环境移到同步块中执行

 

编译器可以合法的,也是合理的,将instance = temp移动到最里层的同步块内,这样就出现了上个版本同样的问题。

 

JDK1.5及其后续版本中,扩充了volatile语义,系统将不允许对 写入一个volatile变量的操作与其之前的任何读写操作 重新排序,也不允许将 读取一个volatile变量的操作与其之后的任何读写操作 重新排序。

 

jdk1.5及其后的版本中,可以将instance 设置成volatile以让双重检查锁定生效,如下:

public class Singleton {

    private static volatile Singleton instance = null;

    private Singleton(){}

   

    public static Singleton getInstance() {

       if(instance == null) {

           synchronized(Singleton.class) {

              if(instance == null) {

                  instance = new Singleton();

              }

           }

       }

       return instance;

    }

}

 

需要注意的是:在JDK1.4以及之前的版本中,该方式仍然有问题。

 

16
8
分享到:
评论
20 楼 zhangdong92 2017-02-08  
我有一个想法,增加一个字段boolean initFinish=false;
每次读取instance时不再判断instance==null,而是判断initFinish是否为true。在初始化时,
instance=new Instance();
initFinish=true;

这样是否可以解决?
19 楼 qq32933432 2017-01-20  
将instance = temp移动到最里层的同步块内,这样就出现了上个版本同样的问题。

请问下这里是什么意思,上个版本是因为对象还没有构造完成另一个线程读取了这个对象,所以导致对象不完整,但是这里我觉得应该没问题吧?在代码走到instance = temp这一段的时候前面new Singleton();肯定已经执行完毕了呀  对象已经成功构建了,并不会造成对象不完整吧?
18 楼 liuInsect 2014-03-12  
"原因在于:初始化Singleton  和 将对象地址写到instance字段 的顺序是不确定的。在某个线程new Singleton()时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化;此时若另外一个线程来调用getInstance,取到的就是状态不正确的对象。"

看不懂。
17 楼 happyJavaer 2012-12-08  
freish 写道
引用
此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化;

我的意思是说:在已经初始化完成了,调用了构造方法了,不会还没有初始化吧



这里的初始化显然同“装载连接初始化”中的初始化含义不一样

假设有以下语句:
String str = new String();

是分成下面几条指令执行的:
0:   new     #2; //class java/lang/String
1:   dup
2:   invokespecial   #3; //Method java/lang/String."<init>":()V
3:   astore_1

到第2步才是调用构造方法,第三步是给引用赋值,但第2步与第3步的顺序能否保证呢,有没有可能被reorder呢


你好,请问一下
synchronized(Singleton.class) {

              if(instance == null) {

                  instance = new Singleton();

              }
}

          
这段代码不是说明了若线程A取得锁,执行实例化的操作的话,线程B必须要等到线程A执行完这段代码后并释放锁后才能进入同步块吧?是不是说线程A中只取得了把单例对象的地址赋值给instance而没有执行对象的init方法就把锁释放了让线程B可以取得锁?
16 楼 freish 2012-09-25  
原文以及原话:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

JDK5 and later extends the semantics for volatile so that the system will not allow a write of a volatile to be reordered with respect to any previous read or write, and a read of a volatile cannot be reordered with respect to any following read or write.


iceman1952 写道
引用
在JDK1.5及其后续版本中,扩充了volatile语义,系统将不允许对 写入一个volatile变量的操作与其之前的任何读写操作 重新排序,也不允许将 读取一个volatile变量的操作与其之后的任何读写操作 重新排序。

错。
JDK1.5及其后续版本中,多个 volatile field 间当然还是不能 reorder(和JDK1.4一样)。但,volatile field和non-volatile field间的reorder却没那么容易了(也就是依然可以reorder)。
举例:线程B读取 线程A写入的volatile v时,线程B可以看到 线程A在写入v之前的所有操作(对于线程A来讲, 其“写入v 这个操作”以及“写入v之前的 所有操作”依然还是可以reorder的)

http://www.cs.umd.edu/users/pugh/java/memoryModel/jsr-133-faq.html#volatile
引用
In effect, because the new memory model places stricter constraints on reordering of volatile field accesses with other field accesses, volatile or not, anything that was visible to thread A when it writes to volatile field f becomes visible to thread B when it reads f.
15 楼 iceman1952 2012-09-25  
引用
在JDK1.5及其后续版本中,扩充了volatile语义,系统将不允许对 写入一个volatile变量的操作与其之前的任何读写操作 重新排序,也不允许将 读取一个volatile变量的操作与其之后的任何读写操作 重新排序。

错。
JDK1.5及其后续版本中,多个 volatile field 间当然还是不能 reorder(和JDK1.4一样)。但,volatile field和non-volatile field间的reorder却没那么容易了(也就是依然可以reorder)。
举例:线程B读取 线程A写入的volatile v时,线程B可以看到 线程A在写入v之前的所有操作(对于线程A来讲, 其“写入v 这个操作”以及“写入v之前的 所有操作”依然还是可以reorder的)

http://www.cs.umd.edu/users/pugh/java/memoryModel/jsr-133-faq.html#volatile
引用
In effect, because the new memory model places stricter constraints on reordering of volatile field accesses with other field accesses, volatile or not, anything that was visible to thread A when it writes to volatile field f becomes visible to thread B when it reads f.
14 楼 freish 2012-03-19  
引用
此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化;

我的意思是说:在已经初始化完成了,调用了构造方法了,不会还没有初始化吧



这里的初始化显然同“装载连接初始化”中的初始化含义不一样

假设有以下语句:
String str = new String();

是分成下面几条指令执行的:
0:   new     #2; //class java/lang/String
1:   dup
2:   invokespecial   #3; //Method java/lang/String."<init>":()V
3:   astore_1

到第2步才是调用构造方法,第三步是给引用赋值,但第2步与第3步的顺序能否保证呢,有没有可能被reorder呢
13 楼 glmylove 2012-03-18  
freish 写道
glmylove 写道
引用
原因在于:初始化Singleton  和 将对象地址写到instance字段 的顺序是不确定的。在某个线程new Singleton()时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化;此时若另外一个线程来调用getInstance,取到的就是状态不正确的对象。


这段话不太认同,JAVA的类加载、连接、初始化这个过程是线程安全的,在初始化之前,是不可能使用这个类的吧?



所谓的线程安全不就是保证不会有第二个线程对该类进行装载连接初始化么?

而在new对象的时候,装载-连接-初始化<clinit>必然已经完成了,换而言之,new的时候与装载-连接-初始化已经木有关系了

new的时候,只涉及内存空间的分配赋默认值以及<init>的调用了

在赋默认值之后,调用<init>之前,个人认为是有可能和后面的指令reorder的吧


引用
此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化;


我的意思是说:在已经初始化完成了,调用了构造方法了,不会还没有初始化吧
12 楼 freish 2012-03-18  
glmylove 写道
引用
原因在于:初始化Singleton  和 将对象地址写到instance字段 的顺序是不确定的。在某个线程new Singleton()时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化;此时若另外一个线程来调用getInstance,取到的就是状态不正确的对象。


这段话不太认同,JAVA的类加载、连接、初始化这个过程是线程安全的,在初始化之前,是不可能使用这个类的吧?



所谓的线程安全不就是保证不会有第二个线程对该类进行装载连接初始化么?

而在new对象的时候,装载-连接-初始化<clinit>必然已经完成了,换而言之,new的时候与装载-连接-初始化已经木有关系了

new的时候,只涉及内存空间的分配赋默认值以及<init>的调用了

在赋默认值之后,调用<init>之前,个人认为是有可能和后面的指令reorder的吧
11 楼 glmylove 2012-03-18  
引用
原因在于:初始化Singleton  和 将对象地址写到instance字段 的顺序是不确定的。在某个线程new Singleton()时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化;此时若另外一个线程来调用getInstance,取到的就是状态不正确的对象。


这段话不太认同,JAVA的类加载、连接、初始化这个过程是线程安全的,在初始化之前,是不可能使用这个类的吧?
10 楼 dddd8579 2011-10-11  
mark下
9 楼 adzshai 2011-09-20  
不幸的是java程序员,
写个恶汉单例也那么多规则
8 楼 Reset 2011-07-18  
volatile 关键字修饰类成员变量
7 楼 csuyux 2011-06-15  
star022 写道
public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

恩,饿汉式单例更符合JAVA语言特点。
6 楼 star022 2011-06-07  
public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}
5 楼 freish 2011-04-20  
xbclll 写道
很好的想法!不幸的是,该方案也未能解决问题之根本:



原因在于:初始化Singleton  和 将对象地址写到instance字段 的顺序是不确定的。在某个线程new Singleton()时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化;此时若另外一个线程来调用getInstance,取到的就是状态不正确的对象。



求教楼主:以上这段话我不懂什么意思,难道instance = new Singleton();不是先new出这个对象,然后再付给instance这个句柄嘛?

因为new在jvm层次是由多个指令完成的,所以允许在 给对象分配内存之后,未调用构造方法之前给引用赋值
4 楼 xbclll 2011-04-20  
很好的想法!不幸的是,该方案也未能解决问题之根本:



原因在于:初始化Singleton  和 将对象地址写到instance字段 的顺序是不确定的。在某个线程new Singleton()时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化;此时若另外一个线程来调用getInstance,取到的就是状态不正确的对象。



求教楼主:以上这段话我不懂什么意思,难道instance = new Singleton();不是先new出这个对象,然后再付给instance这个句柄嘛?
3 楼 renwolang521 2011-04-20  
还有一种内部类的写法
2 楼 freish 2011-04-19  
woaiwofengkuang 写道
1.5以上用enum很简单的


主要不是为讨论单例,只是为了简单说说java内存模型
1 楼 woaiwofengkuang 2011-04-19  
1.5以上用enum很简单的

相关推荐

Global site tag (gtag.js) - Google Analytics