双重检查锁定及单例模式

单例创建

要理解双重检查锁定是从哪里起源的,就必须理解通用单例创建,如清单 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.*;
class Singleton {
private static Singleton instance;
private Vector v;
private boolean inUse;

private Singleton() {
v = new Vector();
v.addElement(new Object());
inUse = true;
}

public static Singleton getInstance() {
if (instance == null) //1
instance = new Singleton(); //2
return instance; //3
}
}

此类的设计确保只创建一个 Singleton 对象。构造函数被声明为 private,getInstance()方法只创建一个对象。这个实现适合于单线程程序。然而,当引入多线程时,就必须通过同步来保护getInstance()方法。如果不保护getInstance() 方法,则可能返回 Singleton对象的两个不同的实例。假设两个线程并发调用 getInstance()方法并且按以下顺序执行调用:

  1. 线程1调用getInstance()方法并判断出instance在//1处为 null。
  2. 线程1进入if代码块,但在执行//2处的代码行时被线程2预占。
  3. 线程2调用getInstance()方法并在//1处判断出instance为 null。
  4. 线程2进入if代码块并创建一个新的Singleton对象并在//2处将变量instance分配给这个新对象。
  5. 线程2 在//3处返回Singleton对象引用。
  6. 线程2被线程1预占。
  7. 线程1在它停止的地方启动,并执行//2代码行,这导致创建另一个Singleton对象。
  8. 线程1在//3处返回这个对象。

结果是getInstance()方法创建了两个Singleton对象,而它本该只创建一个对象。通过同步getInstance()方法从而在同一时间只允许一个线程执行代码,这个问题得以改正,如清单2 所示:

1
2
3
4
5
public static synchronized Singleton getInstance() {
if (instance == null) //1
instance = new Singleton(); //2
return instance; //3
}

清单2中的代码针对多线程访问getInstance()方法运行得很好。然而,其实只有在第一次调用方法时才需要同步。由于只有第一次调用执行了//2处的代码,而只有此行代码需要同步,因此就无需对后续调用使用同步。所有其它调用用于判断instance是否为null,并将其返回。多线程能够安全并发地执行除第一次调用外的所有调用。尽管如此,由于该方法是synchronized的,需要为该方法的每一次调用付出同步的代价,即使只有第一次调用需要同步。

为使此方法更为有效,一个被称为双重检查锁定的习语就应运而生了。这个想法是为了避免对除第一次调用外的所有调用都实行同步的昂贵代价。同步的代价在不同的 JVM 间是不同的。在早期,代价相当高。随着更高级的JVM的出现,同步的代价降低了,但出入synchronized方法或块仍然有性能损失。

因为只有清单2中的//2行需要同步,可以只将其包装到一个同步块中,如清单3所示:

1
2
3
4
5
6
7
8
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
instance = new Singleton();
}
}
return instance;
}

清单3中的代码展示了用多线程加以说明的和清单1相同的问题。当instancenull时,两个线程可以并发地进入if语句内部。然后,一个线程进入synchronized块来初始化instance,而另一个线程则被阻断。当第一个线程退出synchronized块时,等待着的线程进入并创建另一个Singleton对象。注意:当第二个线程进入synchronized块时,它并没有检查instance是否为null

双重检查锁定

为处理清单3中的问题,需要对instance进行第二次检查。这就是“双重检查锁定”名称的由来。将双重检查锁定习语应用到清单3的结果就是清单4。

1
2
3
4
5
6
7
8
9
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) { //1
if (instance == null) //2
instance = new Singleton(); //3
}
}
return instance;
}

双重检查锁定背后的理论是:在//2处的第二次检查使(如清单3中那样)创建两个不同的Singleton对象成为不可能。假设有下列事件序列:

  1. 线程1进入getInstance()方法。
  2. 由于instance为null,线程1在//1处进入synchronized块。
  3. 线程1被线程2预占。
  4. 线程2进入getInstance()方法。
  5. 由于instance仍旧为null,线程2试图获取//1处的锁。然而,由于线程1持有该锁,线程2在//1处阻塞。
  6. 线程2被线程1预占。
  7. 线程1执行,由于在//2处实例仍旧为null,线程1还创建一个 Singleton对象并将其引用赋值给instance。
  8. 线程1退出synchronized块并从getInstance()方法返回实例。
  9. 线程1被线程2预占。
  10. 线程2获取//1处的锁并检查instance是否为null。
  11. 由于instance是非null的,并没有创建第二个Singleton 对象,由线程1创建的对象被返回。

双重检查锁定背后的理论是完美的。但仍存在问题,双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。

上述清单 4 中的//3 行。此行代码创建了一个Singleton对象并初始化变量instance 来引用此对象。这行代码的问题是:在 Singleton构造函数体执行之前,变量instance可能成为非null的。instance = new Singleton(); 这一行代码可以分解为如下的三行伪代码:

1
2
3
memory = allocate();   //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址

上面三行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实发生的)。2和3之间重排序之后的执行时序如下:

1
2
3
4
memory = allocate();   //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址
//注意,此时对象还没有被初始化!
ctorInstance(memory); //2:初始化对象

回到之前的DoubleCheckedLocking示例代码的第7行(instance = new Singleton();)如果发生重排序,另一个并发执行的线程B就有可能判断instance不为null。线程B接下来将访问instance所引用的对象,但此时这个对象可能还没有被A线程初始化!

在知晓了问题发生的根源之后,我们可以想出两个办法来实现线程安全的延迟初始化:1. 不允许2和3重排序;2.允许2和3重排序,但不允许其他线程“看到”这个重排序。

考虑到当前的双重检查锁定不起作用,加入了另一个版本的代码,如清单 7 所示,从而防止刚才看到的无序写入问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) { //1
Singleton inst = instance; //2
if (inst == null) {
synchronized(Singleton.class) { //3
inst = new Singleton(); //4
}
instance = inst; //5
}
}
}
return instance;
}

看着清单7中的代码,此代码试图避免无序写入问题。它试图通过引入局部变量inst和第二个synchronized 块来解决这一问题。由于当前内存模型的定义,清单7中的代码无效。Java语言规范(Java Language Specification,JLS)要求不能将synchronized块中的代码移出来。但是,并没有说不能将 synchronized 块外面的代码移 synchronized 块中。

JIT 编译器会在这里看到一个优化的机会。此优化会删除//4和//5处的代码,组合并且生成清单8中所示的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) { //1
Singleton inst = instance; //2
if (inst == null) {
synchronized(Singleton.class) { //3
//inst = new Singleton(); //4
instance = new Singleton();
}
//instance = inst; //5
}
}
}
return instance;
}

如果进行此项优化,将同样遇到我们之前讨论过的无序写入问题。

基于volatile的双重检查锁定的解决方案

对于前面的基于双重检查锁定来实现延迟初始化的方案(指DoubleCheckedLocking示例代码),只需要做一点小的修改(把instance声明为volatile型),就可以实现线程安全的延迟初始化。请看下面的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SafeDoubleCheckedLocking {
private volatile static Instance instance;

public static Instance getInstance() {
if (instance == null) {
synchronized (SafeDoubleCheckedLocking.class) {
if (instance == null)
instance = new Instance();//instance为volatile,现在没问题了
}
}
return instance;
}
}

基于类初始化的解决方案

JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。

基于这个特性,可以实现另一种线程安全的延迟初始化方案(这个方案被称之为Initialization On Demand Holder idiom):

1
2
3
4
5
6
7
8
9
10
public class InstanceFactory {
private static class InstanceHolder {
public static Instance instance = new Instance();
}

public static Instance getInstance() {
return InstanceHolder.instance ; //这里将导致InstanceHolder类被初始化
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static Singleton getInstance() {
if (instance == null) {
/**
monitorenter
getstatic #2 <Singleton.instance>
ifnonull
new #3
dup
invokespecial #4 <Singleton.<init>>
putstatic #2 <Singleton.instance>
aload_0
monitorexist
**/
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}

更多参考:
http://blog.csdn.net/zero__007/article/details/45644257
http://blog.csdn.net/turkeyzhou/article/details/6179951