在这里倒不是为了讨论单例模式应用什么场合, 主要是说延时加载的单例模式引发的一系列问题,以及该如何解决。
以下对延时加载的问题的分析,参考自:
http://www.ibm.com/developerworks/java/library/j-dcl.html
http://www.iteye.com/topic/157952
以上都提到了延时加载单例模式中的double-checked locking(简称DCL),也即双重锁定失败,以及针对DCL的解决办法。
先来看看延时加载的单例代码:
由于以上没有采用同步,因此多线程环境下可能会创建多个实例。
那么我们加上synchronized,在方法中加了同步块:
同步块包围了单例对象的创建, 看似这样就是个典型的延时加载的单例模式了。但还有一个问题,假设在线程A运行到//1的时候, 被线程B抢占了cpu,并运行到了//3,那么线程A继续执行就又会重复创建对象了,从而就破坏了单例的规则。
ok,那么我们再做一个修改,代码变成这样:
我们看到, 除了//1进行了一次非null检查,在同步块代码//3处又加入了一次检查,这就是double-checked locking (DCL),这样就避免发生像上面的在其他线程又重复创建单例对象的问题,看起来好像是完美了。
不幸的是, 即使是DCL也可能会失败。
instance = new Singleton();
这么简单一行创建对象的代码,就分为几步:
1)Allocate memory,
2)invoke constructor,
3)give reference
如果这几步顺序就是1),2),3) 那没问题,不过JIT编译器不保证2)和3)的顺序, 于是可能变成了1),3),2) 看看这时可能会发生什么:
线程A先进入同步块,执行 instance = new Singleton(), 这时instance 只拿到了ref,但构造器还未执行,退出了同步块;
线程B切入, 这时instance 是 非null的(上面线程A已经分配了ref),于是就return了 instance,而这个instance 却是没有经过构造初始化的,再访问instance就会导致错误。
这就是JITcompilers的out-of-order问题, 导致了初始化和变量赋值的顺序不可预料,产生以上问题。
关于DCL更加详细的分析:
dcl的真正问题在于,它是基于这样的假设:当没有使用同步时读取一个共享变量,可能发生的最坏的事情不过是错误地看到过期值;这种情况下,dcl方法通过占有缩后再检查一次,希望能避免风险。但是,最坏的情况事实上比这还糟糕——线程可能看到引用的当前值(注:指),但是对象的状态值确认过期的。这意味着对象可以被观察到,但是却处于无效或者错误的状态。
下面逐句解释一下:
当没有使用同步时读取一个共享变量
注:指第一个m_instance== null的判断
,可能发生的最坏的事情不过是错误地看到过期值
注:即其他线程已经修改了m_instance,从默认的null赋值为新建对象的句柄,但是由于没有使用同步,因此当前线程可能无法看到修改后的值
;这种情况下,dcl方法通过占有锁后再检查一次,希望能避免风险
注:使用synchronized关键字,获取到锁后,再读取m_instance,这个时候读取的值由于有了同步肯定是正确的,因此第二个m_instance == null的判断可以正确进行,之后的创建对象和赋值也是正确的
但是,最坏的情况事实上比这还糟糕——线程可能看到引用的当前值
注:指第一个m_instance== null不成立时,即这个线程看到了赋值后的m_instance引用的值,然后就直接return m_instance了。
,但是对象的状态值确是过期的。
注:这里翻译的过期有些不好理解,对照英文原文是stale values for the object's state,stale的意思是"不新鲜的, 陈腐的, 疲倦的, 陈旧的".
这意味着对象可以被观察到,但是却处于无效或者错误的状态。
注:这里涉及到一个安全发布的概念,即由于存在重排序的可能性,m_instance = new LazySingleton();中创建对象的过程(即写入对象域)和给m_instance赋值(写如新对象引用)的过程可能发生重排序,出现对象部分创建后就发现m_instance赋值,然后其他线程得到m_instance的值,并通过这个对象访问到还处于部分创建过程中的对象。
在阎宏的《Java与模式》中关于双重检查锁定失效的描述,阐述的是同样的一个道理:
“在Java编译器中,LazySingleton类的初始化与m_instance变量赋值的顺序不可预料。如果一个线程在没有同步化的条件下读取m_instance引用,并调用这个对象的方法的话,可能会发现对象的初始化过程尚未完成,从而造成崩溃。”
好了,到这里所有问题都列出来了, 已经能够看到单例模式用延时加载变得如此麻烦和让人担心。
先撇开延时加载,对于单例模式而言,如果不关心是否必须采用延时加载, 最简单的方法无外乎是静态加载:
这样做既easy又safe。
还是看看如何完善延时加载的单例模式:
1。java 5以前不支持dcl。
2。java 5以后,根据spec,用volatile来声明那个static变量就可以。
3。实际上,一些vm不见得正确实现了volatile语义(比如x86上volatile就没有实现memory barrier),所以即使volatile也不见得好使。
4。最好的办法,不是用volatile,而是保证LazySingleton是immutable的,那么只要是java 5就保证必然成功。可以参考String的代码(虽然没用dcl,但是利用了final的语义来保证安全)。具体spec请参见: http://www.google.com/url?q=http://www.cs.umd.edu/users/jmanson/java/journal.pdf&sa=X&oi=unauthorizedredirect&ct=targetlink&ust=1201014498800543&usg=AFQjCNH1rrzRUKbvIfNL_3JDQ9unbZQydg
5。如果万一LazySingleton不是immutable。可以用一个final place holder来搞:
如果是jdk5, 单例对象用final和volatile可以解决dcl问题, 不过final更加推荐,
而且一般单例的对象都可以用final。
其实还有一个通用的方法:
DCL的替代Initialize-On-Demand:
只有调用getInstance方法,使用LazyFoo类, 才会初始化LazyFoo,也达到了lazy的目的,而且不需要jdk5。
以下对延时加载的问题的分析,参考自:
http://www.ibm.com/developerworks/java/library/j-dcl.html
http://www.iteye.com/topic/157952
以上都提到了延时加载单例模式中的double-checked locking(简称DCL),也即双重锁定失败,以及针对DCL的解决办法。
先来看看延时加载的单例代码:
- public static Singleton getInstance()
- {
- if (instance == null) //1
- instance = new Singleton(); //2
- return instance; //3
- }
由于以上没有采用同步,因此多线程环境下可能会创建多个实例。
那么我们加上synchronized,在方法中加了同步块:
- public static Singleton getInstance()
- {
- if (instance == null) //1
- {
- synchronized(Singleton.class) { //2
- instance = new Singleton();
- }
- }
- return instance; //3
- }
同步块包围了单例对象的创建, 看似这样就是个典型的延时加载的单例模式了。但还有一个问题,假设在线程A运行到//1的时候, 被线程B抢占了cpu,并运行到了//3,那么线程A继续执行就又会重复创建对象了,从而就破坏了单例的规则。
ok,那么我们再做一个修改,代码变成这样:
- public static Singleton getInstance()
- {
- if (instance == null) //1
- {
- synchronized(Singleton.class) {
- if (instance == null) //2
- instance = new Singleton(); //3
- }
- }
- return instance;
- }
我们看到, 除了//1进行了一次非null检查,在同步块代码//3处又加入了一次检查,这就是double-checked locking (DCL),这样就避免发生像上面的在其他线程又重复创建单例对象的问题,看起来好像是完美了。
不幸的是, 即使是DCL也可能会失败。
instance = new Singleton();
这么简单一行创建对象的代码,就分为几步:
1)Allocate memory,
2)invoke constructor,
3)give reference
如果这几步顺序就是1),2),3) 那没问题,不过JIT编译器不保证2)和3)的顺序, 于是可能变成了1),3),2) 看看这时可能会发生什么:
线程A先进入同步块,执行 instance = new Singleton(), 这时instance 只拿到了ref,但构造器还未执行,退出了同步块;
线程B切入, 这时instance 是 非null的(上面线程A已经分配了ref),于是就return了 instance,而这个instance 却是没有经过构造初始化的,再访问instance就会导致错误。
这就是JITcompilers的out-of-order问题, 导致了初始化和变量赋值的顺序不可预料,产生以上问题。
关于DCL更加详细的分析:
引用
dcl的真正问题在于,它是基于这样的假设:当没有使用同步时读取一个共享变量,可能发生的最坏的事情不过是错误地看到过期值;这种情况下,dcl方法通过占有缩后再检查一次,希望能避免风险。但是,最坏的情况事实上比这还糟糕——线程可能看到引用的当前值(注:指),但是对象的状态值确认过期的。这意味着对象可以被观察到,但是却处于无效或者错误的状态。
下面逐句解释一下:
当没有使用同步时读取一个共享变量
注:指第一个m_instance== null的判断
,可能发生的最坏的事情不过是错误地看到过期值
注:即其他线程已经修改了m_instance,从默认的null赋值为新建对象的句柄,但是由于没有使用同步,因此当前线程可能无法看到修改后的值
;这种情况下,dcl方法通过占有锁后再检查一次,希望能避免风险
注:使用synchronized关键字,获取到锁后,再读取m_instance,这个时候读取的值由于有了同步肯定是正确的,因此第二个m_instance == null的判断可以正确进行,之后的创建对象和赋值也是正确的
但是,最坏的情况事实上比这还糟糕——线程可能看到引用的当前值
注:指第一个m_instance== null不成立时,即这个线程看到了赋值后的m_instance引用的值,然后就直接return m_instance了。
,但是对象的状态值确是过期的。
注:这里翻译的过期有些不好理解,对照英文原文是stale values for the object's state,stale的意思是"不新鲜的, 陈腐的, 疲倦的, 陈旧的".
这意味着对象可以被观察到,但是却处于无效或者错误的状态。
注:这里涉及到一个安全发布的概念,即由于存在重排序的可能性,m_instance = new LazySingleton();中创建对象的过程(即写入对象域)和给m_instance赋值(写如新对象引用)的过程可能发生重排序,出现对象部分创建后就发现m_instance赋值,然后其他线程得到m_instance的值,并通过这个对象访问到还处于部分创建过程中的对象。
在阎宏的《Java与模式》中关于双重检查锁定失效的描述,阐述的是同样的一个道理:
“在Java编译器中,LazySingleton类的初始化与m_instance变量赋值的顺序不可预料。如果一个线程在没有同步化的条件下读取m_instance引用,并调用这个对象的方法的话,可能会发现对象的初始化过程尚未完成,从而造成崩溃。”
好了,到这里所有问题都列出来了, 已经能够看到单例模式用延时加载变得如此麻烦和让人担心。
先撇开延时加载,对于单例模式而言,如果不关心是否必须采用延时加载, 最简单的方法无外乎是静态加载:
- class Singleton
- {
- private Vector v;
- private boolean inUse;
- private static Singleton instance = new Singleton();
- private Singleton()
- {
- v = new Vector();
- inUse = true;
- //...
- }
- public static Singleton getInstance()
- {
- return instance;
- }
- }
这样做既easy又safe。
还是看看如何完善延时加载的单例模式:
ajoo 写道
1。java 5以前不支持dcl。
2。java 5以后,根据spec,用volatile来声明那个static变量就可以。
3。实际上,一些vm不见得正确实现了volatile语义(比如x86上volatile就没有实现memory barrier),所以即使volatile也不见得好使。
4。最好的办法,不是用volatile,而是保证LazySingleton是immutable的,那么只要是java 5就保证必然成功。可以参考String的代码(虽然没用dcl,但是利用了final的语义来保证安全)。具体spec请参见: http://www.google.com/url?q=http://www.cs.umd.edu/users/jmanson/java/journal.pdf&sa=X&oi=unauthorizedredirect&ct=targetlink&ust=1201014498800543&usg=AFQjCNH1rrzRUKbvIfNL_3JDQ9unbZQydg
5。如果万一LazySingleton不是immutable。可以用一个final place holder来搞:
- 1. class LazySingleton {
- 2. private LazySingleton() {}
- 3. private static class FinalPlaceHolder {
- 4. final LazySingleton singleton;
- 5. FinalPlaceHolder(LazySingleton s) {
- 6. this.singleton = s;
- 7. }
- 8. }
- 9. private static FinalPlaceHolder holder = null;
- 10. public static LazySingleton instance() {
- 11. if (holder == null) {
- 12. synchronized(LazySingleton.class) {
- 13. if (holder == null) {
- 14. holder = new FinalPlaceHolder(new LazySingleton());
- 15. }
- 16. }
- 17. }
- 18. return holder.singleton;
- 19. }
- 20. }
如果是jdk5, 单例对象用final和volatile可以解决dcl问题, 不过final更加推荐,
而且一般单例的对象都可以用final。
其实还有一个通用的方法:
DCL的替代Initialize-On-Demand:
- public class Foo {
- // 似有静态内部类, 只有当有引用时, 该类才会被装载
- private static class LazyFoo {
- public static Foo foo = new Foo();
- }
- public static Foo getInstance() {
- return LazyFoo.foo;
- }
- }
只有调用getInstance方法,使用LazyFoo类, 才会初始化LazyFoo,也达到了lazy的目的,而且不需要jdk5。