在单例模式中,很多时候会使用延迟初始化(lazy initialization),即在第一次使用时才初始化对象,以推迟高开销的对象初始化工作。下面是非线程安全的延迟初始化代码:
public class Singleton {
private static Singleton uniqueSingleton;
private Singleton() {
}
public static Singleton getInstance() {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton();
}
return uniqueSingleton;
}
}
这段代码的问题在于,在多线程的情况下,可能会出现如下访问顺序。
Time | Thread A | Thread B |
---|---|---|
T1 | 检查到uniqueSingleton为空 | |
T2 | 检查到uniqueSingleton为空 | |
T3 | 初始化对象A | |
T4 | 返回对象A | |
T5 | 初始化对象B | |
T6 | 返回对象B |
这两个线程就持有了这个类的两个不同实例,违背了单例模式的初衷。
我们可以用synchronized
关键字修饰方法,加上同步锁。
public class Singleton {
private static Singleton uniqueSingleton;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton();
}
return uniqueSingleton;
}
}
但是synchronized
会导致性能开销,而且其实只用在第一次初始化时加锁,后面再调用时,都没有必要加锁了。
那我们可以考虑做一下优化,使用双重检查锁——在加锁前先做一次检查,如果对象初始化已经完成,就没有必要再加锁了,代码如下。
public class Singleton {
private static Singleton uniqueSingleton;
private Singleton() {
}
public static Singleton getInstance() {
if (null == uniqueSingleton) {
synchronized (Singleton.class) {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton(); // error
}
}
}
return uniqueSingleton;
}
}
这样做,似乎完美解决了产生多个对象和性能开销的问题:
实则不然。
上面双重检查锁的代码其实是错误的,问题的根源在于初始化对象并不是原子操作,并且可能出现重排序。初始化对象的代码,即上面代码中标记了error的那一行,可以分解为三个步骤:
i. 分配内存空间
ii. 初始化对象
iii. 将对象指向刚分配的内存空间
而在一些JIT编译器上,为了性能原因,第二步和第三步可能被重排序。两个线程可能出现如下的执行顺序:
Time | Thread A | Thread B |
---|---|---|
T1 | 检查到uniqueSingleton为空 | |
T2 | 获取锁 | |
T3 | 再次检查到uniqueSingleton为空 | |
T4 | 为uniqueSingleton分配内存空间 | |
T5 | 将uniqueSingleton指向内存空间 | |
T6 | 检查到uniqueSingleton不为空 | |
T7 | 访问uniqueSingleton(未初始化的对象) | |
T8 | 初始化uniqueSingleton |
这时,线程B会读到一个未被初始化的对象(一块具有随机值的内存)。为了解决这个问题,只需要用volatile
关键字修饰变量uniqueSingleton
就可以了,正确的双重检查锁的实现代码如下。
public class Singleton {
private volatile static Singleton uniqueSingleton;
private Singleton() {
}
public static Singleton getInstance() {
if (null == uniqueSingleton) {
synchronized (Singleton.class) {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton();
}
}
}
return uniqueSingleton;
}
}
使用volatile
后,重排序被禁止,所有的写操作(write)都会发生在读操作(read)之前。
JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以实现另一种线程安全的延迟初始化方案(这个方案被称之为initialization-on-demand holder idiom),代码如下。
public class Singleton {
private static class SingletonHolder {
public static Singleton uniqueSingleton = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.uniqueSingleton;
}
}
这个方案利用了Java中类的初始化锁(initialization lock)LC。根据JLS(Java SE 8 Edition)中“§12.4.1 When Initialization Occurs”一节中的描述:
A class or interface type T will be initialized immediately before the first occurrence of any one of the following:
- T is a class and an instance of T is created.
- A static method declared by T is invoked.
- A static field declared by T is assigned.
- A static field declared by T is used and the field is not a constant variable (§4.12.4).
- T is a top level class (§7.6) and an assert statement (§14.10) lexically nested within T (§8.1.3) is executed.
上述代码中当getInstance()
方法第一次被调用时,符合上面的第四种情况,类SingletonHolder
被初始化,会使用LC进行同步。这样就利用了由JVM实现的类初始化锁,实现了延迟初始化的线程安全版本。关于LC的具体细节可以查看参考文献[5]中的“§12.4.2. Detailed Initialization Procedure”一节。
[1] 双重检查锁定与延迟初始化
http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization#anch136785
[2] Java中的双重检查锁(double checked locking)
https://www.cnblogs.com/xz816111/p/8470048.html
[3] Double-checked locking and the Singleton pattern
https://www.ibm.com/developerworks/java/library/j-dcl/index.html
[4] Double-checked locking
https://en.wikipedia.org/wiki/Double-checked_locking
[5] The Java Language Specification, Java SE 8 Edition
https://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4