Double-checked locking真的有效吗?
作者: zsxwing 日期: 2011-04-29 10:48:06
在很多设计模式的书籍中,我们都可以看到类似下面的单例模式的实现代码,一般称为Double-checked locking(DCL)
01 | public class Singleton { |
03 | private static Singleton instance; |
09 | public static Singleton getInstance() { |
10 | if (instance == null ) { //1 |
11 | synchronized (Singleton. class ) { //2 |
12 | if (instance == null ) { //3 |
13 | instance = new Singleton(); //4 |
这样子的代码看起来很完美,可以解决instance的延迟初始化。只是,事实往往不是如此。
问题在于instance = new Singleton();这行代码。
在我们看来,这行代码的意义大概是下面这样子的
mem = allocate(); //收集内存
ctorSingleton(mem); //调用构造函数
instance = mem; //把地址传给instance
这行代码在Java虚拟机(JVM)看来,却可能是下面的三个步骤(乱序执行的机制):
mem = allocate(); //收集内存
instance = mem; //把地址传给instance
ctorSingleton(instance); //调用构造函数
下面我们来假设一个场景。
- 线程A调用getInstance函数并且执行到//4。但是线程A只执行到赋值语句,还没有调用构造函数。此时,instance已经不是null了,但是对象还没有初始化。
- 很不幸线程A这时正好被挂起。
- 线程B获得执行的权力,然后也开始调用getInstance。线程B在//1发现instance已经不是null了,于是就返回对象了,但是这个对象还没有初始化,于是对这个对象进行操作就出错了。
问题就出在instance被提前初始化了。
解决方案一,不使用延迟加载:
01 | public class Singleton { |
03 | private static Singleton instance = new Singleton(); |
09 | public static Singleton getInstance() { |
JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕。
解决方案二,利用一个内部类来实现延迟加载:
01 | public class Singleton { |
07 | private static class SingletonContainer { |
08 | private static Singleton instance = new Singleton(); |
11 | public static Singleton getInstance() { |
12 | return SingletonContainer.instance; |
这两种方案都是利用了JVM的类加载机制的互斥。
方案二的延迟加载实现是因为,只有在第一次调用Singleton.getInstance()函数时,JVM才会去加载SingletonContainer,并且初始化instance。
不只Java存在这个问题,C/C++由于CPU的乱序执行机制,也同样存在这样的问题。
抱歉,我之前的理解有误,DCL在Java中失效的原因是JIT比较激进的优化导致的,在C/C++并不会由于CPU的乱序执行(调用构造函数和赋值这两个操作对CPU来说绝对不会乱序的)产生这个问题。
暂时不知道Java对于这个问题是否修复了。