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

阎宏在《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}
实在是不太清楚,请高手指教。

呵呵,第13、14行标处,本想加粗,没想没成功

>强烈建议大家不要再花时间在实现双重检查成例上
是这样,不必在这上面钻牛角尖,而且单例模式在实际应用中已经逐步被Ioc容器替代,实用价值不高。

你的改法,肯定会被编译器优化掉,或者被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?

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

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

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

>而且单例模式在实际应用中已经逐步被Ioc容器替代,实用价值不高

小弟刚从C++转型C#,入道尚浅,刚开始迷恋模式。banq兄所说的Ioc容器是否在C下也有。小弟本打算在系统中大量用到Singleton,理由是系统中很多的配置、常用数据、调度控制型模块从约束上的确应该只有一个实例。象常用数据,还可以减少反复读取数据库的操作。

>如果我的需求必须计较 SYNCHRONIZED 所带来的几百个 CPU CLOCK 的性能损失,又怎么可能容忍 XML PARSING 和 REFLECTION?

Kyle_Yin兄所说也有道理。XML PARSING和REFLECTION的确很慢,两者都是拿性能来换取灵活性的做法。因此对于它们的使用,我们尽量减少用在调用频率高的代码里。我现在正在开发的平台里也正充分享用到这样个活宝,但都是用在数据加载时,数据加载的频率设计时尽可能减少,因此它们带来的性能损失可以忽略不计。但对于单例模式,由于我大量用在访问频率高的常用数据和中央调度上。对于万级以上的数据处理,同步带来的几百个CPU CLOCK将变得十分可观。

前面看到讨坛里有贴子谈到JAVA现有的一些架构的性能问题。其实就是因为大部分人都有着拿性能来换系统架构的优越,拿硬件来换性能,拿钱来换硬件的思想。可大家是否考虑过,完全忽略性能的设计比考虑过性能的设计在性能上的损失不是倍数级,而是指数级的(如果朋友们有疑惑我可以另行论证)。而目前硬件的发展在摩尔定律(晶体管数量将每两年增加一倍)下,只是十分可怜的线性增长。把目光跳出微观细节,不代表完全不考虑性能,因此对于DCL的问题,我个人认为并不是想钻牛角尖,而是对于我所开发平台的负责任的一种点滴体现。

风魂说得有道理,性能和设计是两只手都要硬,关键是平衡策略,具体问题具体决策。