在很多设计模式的书籍中,我们都可以看到类似下面的单例模式的实现代码,一般称为Double-checked locking(DCL)
01 |
public class
Singleton { |
03 |
private
static Singleton instance; |
09 |
public
static Singleton getInstance() { |
10 |
if
(instance == null ) { |
11 |
synchronized
(Singleton. class ) { |
12 |
if
(instance == null ) { |
13 |
instance =
new Singleton(); |
这样子的代码看起来很完美,可以解决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的乱序执行机制,也同样存在这样的问题。