再谈双重检查成例的懒汉式单例模式

05-11-24 风魂
阎宏在《Java与模式》强烈建议大家不要再花时间在实现双重检查成例上,理由是:

“令人吃惊的是,在C语言里得到普遍应用的双重检查成例在多数的Java语言编译器里并不成立。上面使用了双重检查成例的“懒汉式”单例类,不能工作的基本原因在于,在Java编译器中,LazySingleton类的初始化与m_instance变量赋值的顺序不可预料。如果一个线程在没有同步化的条件下读取m_instance引用,并调用这个对象的方法的话,可能会发现对象的初始化过程尚未完成,从而造成崩溃。”

1 public class LazySingleton

2 {

3 private static LazySingleton m_instance = null;

4 private LazySingleton(){}

5 public static LazySingleton getInstance()

6 {

7 if(m_instance == null)

8 {

9 synchronized(LazySingleton.class)

10 {

11 if(m_instance == null)

12 {

13 m_instance = new LazySingleton();

14 }

15 }

16 }

17 return m_instance;

18 }

19}

我看了不是很明白,是不是指new LazySingleton()在执行构造函数和m_instance = ....这个赋值语句的顺序不确定。可能会出现new LazySingleton() 先分配好内存地址,然的执行了 m_instance = “内存地址”,再执行构造函数里的代码,这样会造成m_instance先被赋了值。其它线程一量这时候访问第7行就会出错。如果是这样,我们是不是可以改成以下:

1 public class LazySingleton

2 {

3 private static LazySingleton m_instance = null;

4 private LazySingleton(){}

5 public static LazySingleton getInstance()

6 {

7 if(m_instance == null)

8 {

9 synchronized(LazySingleton.class)

10 {

11 if(m_instance == null)

12 {

13 LazySingleton temp = new LazySingleton();

14 m_instance = temp;

15 }

16 }

17 }

18 return m_instance;

19 }

20}

实在是不太清楚,请高手指教。

    

风魂
2005-11-24 15:54
呵呵,第13、14行标处,本想加粗,没想没成功

banq
2005-11-30 11:13
>强烈建议大家不要再花时间在实现双重检查成例上

是这样,不必在这上面钻牛角尖,而且单例模式在实际应用中已经逐步被Ioc容器替代,实用价值不高。

Kyle_Yin
2005-12-01 12:36
你的改法,肯定会被编译器优化掉,或者被JVM优化掉,或者被CPU优化掉(如果你的CPU是486 以上的,呵呵)。

阎宏描述的原因是确切的。

不仅如此(构造函数与 instance 变量赋值的顺序完全取决于编译器实现),编译器产生的代码也可能被CPU再次做乱序和并行(out-of-order / parallelism)优化。现代CPU,包括各款奔腾在内,都使用乱序和并行来尽量避免流水线等待内存,IO,以提高执行效率。

另外,一个像样的CPU,是不会把那个 TEMP 写到内存里,再回头花费几百个 CPU CLOCK 从内存里拿回来,赋值给 m_instance 寄存器,然后再把 m_instance 写回内存的。其实远远不到 CPU 的层面,编译器已经会发现这样很不核算从而通通优化调。即使编译器不优化,CPU也不会这么做,而是把m_instance写到缓存里。既然牵扯到缓存,如果你有两个以上的物理CPU,或者两个以上的物理内核,或者两个以上的虚拟内核(所谓超线程),就不能保证各个执行单元看到的是一个值,除非用 synchronized{} 告诉 JVM 需要作 同步/缓存提交。

总而言之,在 JAVA 5 之前的JAVA 内存模型下,如果使用优化编译器,或者使用了并行架构的硬件,那么DCL是“不保证正确”的,无论用什么办法修改,就好像永动机不可能实现一样。

好消息是:包含在 JAVA 5 的 JSR 133 之下,如果把 m_instance 改为 volatile 变量,那么DCL至少是“功能正确”的。因为 JSR 133 更改了 volatile 的语义,使 volatile 变量的读写与 synchronized 有相当的涵义。

坏消息是:如果把 m_instance 改为 volatile 变量,那么DCL虽然功能正确,效率却和在 synchronized block 内部单一检查完全一样,甚至更差一点(因为检查了两次)。所以,DCL 就完全不具有物理意义了。

再次总而言之,DCL 在 JAVA 是没有价值的。值得一提的是,DCL在 C# 里也同样没有意义。抄的就是抄的,连 BUG 都一起抄去了。呵呵。

不过老板关于SPRING的评价在这里并不合乎语境。如果我的需求必须计较 SYNCHRONIZED 所带来的几百个 CPU CLOCK 的性能损失,又怎么可能容忍 XML PARSING 和 REFLECTION?

banq
2005-12-01 14:34
>不过老板关于SPRING的评价在这里并不合乎语境。如果我的需求必须计较 SYNCHRONIZED 所带来的几百个 CPU CLOCK 的性能损失,又怎么可能容忍 XML PARSING 和 REFLECTION?

我说的Ioc容器,Ioc容器替代单态模式,使用Ioc容器,并不意味着“XML PARSING 和 REFLECTION”,这些都是第一次执行结果。

注意,别在这个帖子和我讨论Ioc容器原理。

猜你喜欢
2Go 1 2 下一页