Double-Checked Locking模式的危险性

Double-Checked Locking模式原本是用于改善Single Threaded Execution模式的性能的方法之一。不过在Java中使用Double-Checked Locking模式是很危险的。

Single Threaded Execution模式

getInstance是synchronized的,因此性能并不好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.Date;

public class MySystem {
private static MySystem instance = null;
private Date date = new Date();

private MySystem(){}

public synchronized MySystem getInstance() {
if (instance == null) {
instance = new MySystem();
}

return instance;
}

public Date getDate() {
return date;
}
}

Double-Checked Locking模式

getInstance方法不再是synchronized方法。取而代之的是if语句中编写的一段synchronized代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.Date;

public class MySystem {
private static MySystem instance = null;
private Date date = new Date();

private MySystem(){}

public MySystem getInstance() {
if (instance == null) { //(a)
synchronized (MySystem.class) { //(b)
if (instance == null) { //(c)
instance = new MySystem(); //(d)
}
} //(e)
}

return instance; //(f)
}

public Date getDate() {
return date;
}
}

不能正确地运行的一个原因是,当调用getInstance的返回值的getDate方法时,date字段可能还没有被初始化。

kwXAbt.png

这里创建了一个MySystem的实例。在创建MySystem的实例时,new Date()的值会被赋给实例字段date。如果线程A从synchronized代码块退出后,线程B才进入synchronized代码块,那么线程B也可以看见date的值。但是在(A-4)这个阶段,无法确保线程B可以看见A写入的date字段的值。

接下来,再假设线程B在(B-1)这个阶段的判断结果是instance != null。这样的话,线程B将不进入synchronized代码块,而是立即将instance的值作为返回值return出来。这之后,线程B会在(B-3)这个阶段调用getInstance的返回值的getDate方法。getDate方法的返回值就是date字段的值,因此线程B会引用date字段的值。但是,线程A还没有从synchronized代码块中退出,线程B也没有进入synchronized代码块。因此,无法确保date字段的值对线程B可见。

为什么能够看到instance字段

由于重排序的存在,的确可能会在看到date字段的值之前先看到instance字段的值。

使用volatile会怎样

将instance字段设置为volatile字段后,Double-Checked Locking模式就可以正常工作了。但是,volatile字段的读写性能开销与synchronized几乎相同。本来Double-Checked Locking模式就是用于避免synchronized引起的性能下降的,如果使用了volatile就无法改善性能了。

Initialization On Demand Holder模式

Initialization On Demand Holder模式既不会像Single Threaded Execution模式那样降低性能,也不会带来像Double-Checked Locking模式那样的危险性。

这段程序会使用Holder的“类的初始化”来创建唯一的实例,并确保线程安全。这是因为在Java规范中,类的初始化是线程安全的。

在代码中,并没有使用synchronized和volatile来进行同步,因此性能不会下降。

而且,还使用了嵌套类的延迟初始化。Holder类的初始化在线程刚刚要使用该类时才会开始进行。也就是说,在调用MySystem.getInstance方法前,Holder类不会被初始化,甚至连MySystem的实例都不会创建。因此,使用Initialization On Demand Holder模式可以避免内存浪费。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.Date;

public class MySystem {
private static class Holder {
public static MySystem instance = new MySystem();
}

private Date date = new Date();

private MySystem(){}

public MySystem getInstance() {
return Holder.instance;
}

public Date getDate() {
return date;
}
}