记录生活中的点点滴滴

0%

单例模式volatile

学过 volatile 之后再看单例模式,从 懒加载写法双重检查锁写法 再到 volatile 解决DCL的并发陷阱。

懒加载

一个懒加载的单例模式实现如下:

1
2
3
4
5
6
7
8
9
10
class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if ( instance == null ) { //这里存在竞态条件
instance = new Singleton();
}
return instance;
}
}

竞态条件会导致 instance 引用被多次赋值,使用户得到两个不同的单例。

懒加载DCL

为了解决这个问题,可以使用synchronized关键字将getInstance方法改为同步方法;但这样串行化的单例是不能忍的。所以我们的前辈设计了DCL(Double Check Lock,双重检查锁)机制,使得大部分请求都不会进入阻塞代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if ( instance == null ) { //当instance不为null时,仍可能指向一个“被部分初始化的对象”,用户得到了没有完成初始化的“半个”单例。
synchronized (Singleton.class) {
if ( instance == null ) {
instance = new Singleton();
}
}
}
return instance;
}
}

“看起来”非常完美:既减少了阻塞,又避免了竞态条件。不错,但实际上仍然存在一个问题——当instance不为null时,仍可能指向一个"被部分初始化的对象"

问题出在这行简单的赋值语句:

1
instance = new Singleton();

它并不是一个原子操作。事实上,它可以”抽象“为下面几条JVM指令:

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

上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM可以以“优化”为目的对它们进行重排序,经过重排序后如下:

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

可以看到指令重排之后,操作 3 排在了操作 2 之前,即引用instance指向内存memory时,这段崭新的内存还没有初始化——即,引用instance指向了一个”被部分初始化的对象”。此时,如果另一个线程调用getInstance方法,由于instance已经指向了一块内存空间,从而 if条件判为 false,方法返回instance引用,用户得到了没有完成初始化的“半个”单例。

解决这个该问题,只需要将instance声明为volatile变量。

懒加载DCL加volatile

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

也就是说,在只有DCL没有volatile的懒加载单例模式中,仍然存在着并发陷阱。我确实不会拿到两个不同的单例了,但我会拿到“半个”单例(未完成初始化)。