论坛首页 Java企业应用论坛

不使用volatile,安全DCL式singleton写法

浏览 6382 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2013-12-10  
public class Singleton { 
                      
    private static Singleton singleton; //错误, 这里应该加上volatile关键字, 保证singleton可见性, 从而能够安全发布 
                          
    private Singleton(){ } 
                          
    public static Singleton getInstance(){ 
        // 双重检查加锁 
        if(singleton==null){ 
            synchronized(Singleton.class){ 
                // 延迟实例化,需要时才创建 
                if(singleton==null) 
                    singleton = new Singleton();//错误原因,singleton = new Singleton();不是一个原子操作,有些虚拟机可能出现在类没有完成初始化,singleton 就已经不为null。线程并发时有小概率会产生问题。所以应该加上volatile, 
            } 
        } 
        return singleton; 
    } 

------------------------------------- 
那么不使用volatile关键字能否实现安全DCL呢?答案当然是肯定的,有些文章说,在jdk1.5之前(1.5之前没有volatile关键字),无法实现DCL的singleton,这种说法是错误的。废话少说,上代码 
------------------------------------- 
public class Singleton {   
   
    private static Singleton singleton; // 这类没有volatile关键字   
   
    private Singleton() {   
    }   
   
    public static Singleton getInstance() {   
        // 双重检查加锁   
        if (singleton == null) {   
            synchronized (Singleton.class) {   
                // 延迟实例化,需要时才创建   
                if (singleton == null) {   
                       
                    Singleton temp = null; 
                    try { 
                        temp = new Singleton();   
                    } catch (Exception e) { 
                    } 
                    if (temp != null)   
                        singleton = temp; //为什么要做这个看似无用的操作,因为这一步是为了让虚拟机执行到这一步的时会才对singleton赋值,虚拟机执行到这里的时候,必然已经完成类实例的初始化。所以这种写法的DCL是安全的。由于try的存在,虚拟机无法优化temp!=null 是否为true
                }   
            }   
        }   
        return singleton;   
    } 
}  
   发表时间:2013-12-14  
所谓的懒汉式加载就是一些技术偏执狂搞出来唬人的写法,我觉得根本没必要。。代码既然写了就用得到,早加载晚加载都要加载,加载还只加载一次。。以前看过一篇文章,双重检查加锁并不能确切的实现单例,从此再也不写这种高深的代码啦。。
0 请登录后投票
   发表时间:2013-12-17  
看来楼主还是不理解为什么要加volatile,简单说,你所说的那种方式,理论上也不是线程安全的,注意new的几个过程
0 请登录后投票
   发表时间:2013-12-17  
要么类载入时就创建实例,要么就性能差的synchronized方法,懒汉加载不定就出什么问题……
0 请登录后投票
   发表时间:2013-12-17  
流风suiyue 写道
所谓的懒汉式加载就是一些技术偏执狂搞出来唬人的写法,我觉得根本没必要。。代码既然写了就用得到,早加载晚加载都要加载,加载还只加载一次。。以前看过一篇文章,双重检查加锁并不能确切的实现单例,从此再也不写这种高深的代码啦。。

这种代码是有实际意义的。
比如A模块的启动需要依赖B模块的启动。B模块可能是另外一个系统,那么你就必须使用懒加载方式。
0 请登录后投票
   发表时间:2013-12-17   最后修改:2013-12-17
iamiwell 写道
看来楼主还是不理解为什么要加volatile,简单说,你所说的那种方式,理论上也不是线程安全的,注意new的几个过程

看来是你没有真正理解new的过程。

A a = new A();
...//注意后面代码
new的过程只会影响a的赋值先后顺序,这是因为new的过程不是一个原子造成的。但是不管new的过程有多少,new语句后面的代码都不会在new的过程结束之前执被行。
另外还必须考虑虚拟机优化问题,这里就不仔细说了。
0 请登录后投票
   发表时间:2013-12-17  
qiaoenxin 写道
iamiwell 写道
看来楼主还是不理解为什么要加volatile,简单说,你所说的那种方式,理论上也不是线程安全的,注意new的几个过程

看来是你没有真正理解new的过程。

A a = new A();
...//注意后面代码
new的过程只会影响a的赋值先后顺序,这是因为new的过程不是一个原子造成的。但是不管new的过程有多少,new语句后面的代码都不会在new的过程结束之前执被行。
另外还必须考虑虚拟机优化问题,这里就不仔细说了。


   ...
   20: new #3; //class Singleton
   23: dup
   24: invokespecial #4; //Method "<init>":()V
   <assigns>
   ...

new执行之后,this地址即已经可以使用了,但是构造工作并没有全部完成,在这种情况下,
不管怎么去判断和赋值,都不能保证最终拿到的instance是已经构造好了

至于你的try,这个是防止java编译的优化,而“但是不管new的过程有多少,new语句后面的代码都不会在new的过程结束之前执被行。 ”这句话,是没有根据的,其实你描述的就是一个屏障,没有规范说每个构造器都必须添加屏障,这也是为什么可以使用volatile或final,因为它们就是一个屏障,正如你所说,你也认同构造的过程并不是原子的么

总的来说,理论上这种方式不是线程安全的,但实际中,有可能某些处理器的特殊性,能够保证它的线程安全,或者发生线程安全问题的场景基本不可见

也可以看看类似的文章
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
or
http://ifeve.com/double-checked-locking-with-delay-initialization/
0 请登录后投票
   发表时间:2013-12-17  
iamiwell 写道
qiaoenxin 写道
iamiwell 写道
看来楼主还是不理解为什么要加volatile,简单说,你所说的那种方式,理论上也不是线程安全的,注意new的几个过程

看来是你没有真正理解new的过程。

A a = new A();
...//注意后面代码
new的过程只会影响a的赋值先后顺序,这是因为new的过程不是一个原子造成的。但是不管new的过程有多少,new语句后面的代码都不会在new的过程结束之前执被行。
另外还必须考虑虚拟机优化问题,这里就不仔细说了。


   ...
   20: new #3; //class Singleton
   23: dup
   24: invokespecial #4; //Method "<init>":()V
   <assigns>
   ...

new执行之后,this地址即已经可以使用了,但是构造工作并没有全部完成,在这种情况下,
不管怎么去判断和赋值,都不能保证最终拿到的instance是已经构造好了

至于你的try,这个是防止java编译的优化,而“但是不管new的过程有多少,new语句后面的代码都不会在new的过程结束之前执被行。 ”这句话,是没有根据的,其实你描述的就是一个屏障,没有规范说每个构造器都必须添加屏障,这也是为什么可以使用volatile或final,因为它们就是一个屏障,正如你所说,你也认同构造的过程并不是原子的么

总的来说,理论上这种方式不是线程安全的,但实际中,有可能某些处理器的特殊性,能够保证它的线程安全,或者发生线程安全问题的场景基本不可见

也可以看看类似的文章
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
or
http://ifeve.com/double-checked-locking-with-delay-initialization/




我觉得楼主try那个版本没什么问题。
你注意看下,他是先给try 给temp 赋值 new Singleton(),
而不是给 instance 赋值。
所以不会发生没 new 到一半 instance已经可用的情况。
0 请登录后投票
   发表时间:2013-12-18  
iamiwell 写道
qiaoenxin 写道
iamiwell 写道
看来楼主还是不理解为什么要加volatile,简单说,你所说的那种方式,理论上也不是线程安全的,注意new的几个过程

看来是你没有真正理解new的过程。

A a = new A();
...//注意后面代码
new的过程只会影响a的赋值先后顺序,这是因为new的过程不是一个原子造成的。但是不管new的过程有多少,new语句后面的代码都不会在new的过程结束之前执被行。
另外还必须考虑虚拟机优化问题,这里就不仔细说了。


   ...
   20: new #3; //class Singleton
   23: dup
   24: invokespecial #4; //Method "<init>":()V
   <assigns>
   ...

new执行之后,this地址即已经可以使用了,但是构造工作并没有全部完成,在这种情况下,
不管怎么去判断和赋值,都不能保证最终拿到的instance是已经构造好了

至于你的try,这个是防止java编译的优化,而“但是不管new的过程有多少,new语句后面的代码都不会在new的过程结束之前执被行。 ”这句话,是没有根据的,其实你描述的就是一个屏障,没有规范说每个构造器都必须添加屏障,这也是为什么可以使用volatile或final,因为它们就是一个屏障,正如你所说,你也认同构造的过程并不是原子的么

总的来说,理论上这种方式不是线程安全的,但实际中,有可能某些处理器的特殊性,能够保证它的线程安全,或者发生线程安全问题的场景基本不可见

也可以看看类似的文章
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
or
http://ifeve.com/double-checked-locking-with-delay-initialization/


你的问题7楼已经给你解释了。你需要注意temp变量的作用。

至于你给出的文章,我刚学java的时候就已经看过内容相似的文章。起初我也和你的想法一样,后来相关文章也拜读了不少,才发现那篇文章的问题。

对于A a = new A(); 这个过程大致做了3件事情
1.给实例分配内存。

2.初始实例的构造器

3.将变量指向实例对象的内存空间。
我姑且称为new的三个过程为一个执行单元unit(1,2,3)。虚拟机总是尽可能多的将一个执行单元中可以并发执行的元素(1,2,3)同时提交给cpu乱序执行,提高执行效率。
0 请登录后投票
   发表时间:2013-12-18  
qiaoenxin 写道
iamiwell 写道
qiaoenxin 写道
iamiwell 写道
看来楼主还是不理解为什么要加volatile,简单说,你所说的那种方式,理论上也不是线程安全的,注意new的几个过程

看来是你没有真正理解new的过程。

A a = new A();
...//注意后面代码
new的过程只会影响a的赋值先后顺序,这是因为new的过程不是一个原子造成的。但是不管new的过程有多少,new语句后面的代码都不会在new的过程结束之前执被行。
另外还必须考虑虚拟机优化问题,这里就不仔细说了。


   ...
   20: new #3; //class Singleton
   23: dup
   24: invokespecial #4; //Method "<init>":()V
   <assigns>
   ...

new执行之后,this地址即已经可以使用了,但是构造工作并没有全部完成,在这种情况下,
不管怎么去判断和赋值,都不能保证最终拿到的instance是已经构造好了

至于你的try,这个是防止java编译的优化,而“但是不管new的过程有多少,new语句后面的代码都不会在new的过程结束之前执被行。 ”这句话,是没有根据的,其实你描述的就是一个屏障,没有规范说每个构造器都必须添加屏障,这也是为什么可以使用volatile或final,因为它们就是一个屏障,正如你所说,你也认同构造的过程并不是原子的么

总的来说,理论上这种方式不是线程安全的,但实际中,有可能某些处理器的特殊性,能够保证它的线程安全,或者发生线程安全问题的场景基本不可见

也可以看看类似的文章
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
or
http://ifeve.com/double-checked-locking-with-delay-initialization/


你的问题7楼已经给你解释了。你需要注意temp变量的作用。

至于你给出的文章,我刚学java的时候就已经看过内容相似的文章。起初我也和你的想法一样,后来相关文章也拜读了不少,才发现那篇文章的问题。

对于A a = new A(); 这个过程大致做了3件事情
1.给实例分配内存。

2.初始实例的构造器

3.将变量指向实例对象的内存空间。
我姑且称为new的三个过程为一个执行单元unit(1,2,3)。虚拟机总是尽可能多的将一个执行单元中可以并发执行的元素(1,2,3)同时提交给cpu乱序执行,提高执行效率。

哎,没什么话好说了,请注意,已经拿到this的地址了,当然可以直接给temp,这个时候temp也不是null,也可以赋值给singleton,但这整个过程中,都不能保证构造函数执行完成,这种问题其实讨论的也没什么意思,我只是好心指出来而已
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics