在实现单例模式时,若是未考虑多线程的状况,就容易写出下面的错误代码:html
public class Singleton { private static Singleton uniqueSingleton; private Singleton() { } public Singleton getInstance() { if (null == uniqueSingleton) { uniqueSingleton = new Singleton(); } return uniqueSingleton; } }
在多线程的状况下,这样写可能会致使uniqueSingleton
有多个实例。好比下面这种状况,考虑有两个线程同时调用getInstance()
:java
Time | Thread A | Thread B |
---|---|---|
T1 | 检查到uniqueSingleton 为空 |
|
T2 | 检查到uniqueSingleton 为空 |
|
T3 | 初始化对象A |
|
T4 | 返回对象A |
|
T5 | 初始化对象B |
|
T6 | 返回对象B |
能够看到,uniqueSingleton
被实例化了两次而且被不一样对象持有。彻底违背了单例的初衷。多线程
出现这种状况,第一反应就是加锁,以下:性能
public class Singleton { private static Singleton uniqueSingleton; private Singleton() { } public synchronized Singleton getInstance() { if (null == uniqueSingleton) { uniqueSingleton = new Singleton(); } return uniqueSingleton; } }
这样虽然解决了问题,可是由于用到了synchronized
,会致使很大的性能开销,而且加锁其实只须要在第一次初始化的时候用到,以后的调用都不必再进行加锁。优化
双重检查锁(double checked locking)是对上述问题的一种优化。先判断对象是否已经被初始化,再决定要不要加锁。线程
public class Singleton { private static Singleton uniqueSingleton; private Singleton() { } public Singleton getInstance() { if (null == uniqueSingleton) { synchronized (Singleton.class) { if (null == uniqueSingleton) { uniqueSingleton = new Singleton(); // error } } } return uniqueSingleton; } }
若是这样写,运行顺序就成了:code
执行双重检查是由于,若是多个线程同时了经过了第一次检查,而且其中一个线程首先经过了第二次检查并实例化了对象,那么剩余经过了第一次检查的线程就不会再去实例化对象。htm
这样,除了初始化的时候会出现加锁的状况,后续的全部调用都会避免加锁而直接返回,解决了性能消耗的问题。对象
上述写法看似解决了问题,可是有个很大的隐患。实例化对象的那行代码(标记为error的那行),实际上能够分解成如下三个步骤:排序
可是有些编译器为了性能的缘由,可能会将第二步和第三步进行重排序,顺序就成了:
如今考虑重排序后,两个线程发生了如下调用:
Time | Thread A | Thread B |
---|---|---|
T1 | 检查到uniqueSingleton 为空 |
|
T2 | 获取锁 | |
T3 | 再次检查到uniqueSingleton 为空 |
|
T4 | 为uniqueSingleton 分配内存空间 |
|
T5 | 将uniqueSingleton 指向内存空间 |
|
T6 | 检查到uniqueSingleton 不为空 |
|
T7 | 访问uniqueSingleton (此时对象还未完成初始化) |
|
T8 | 初始化uniqueSingleton |
在这种状况下,T7时刻线程B对uniqueSingleton
的访问,访问的是一个初始化未完成的对象。
public class Singleton { private volatile static Singleton uniqueSingleton; private Singleton() { } public Singleton getInstance() { if (null == uniqueSingleton) { synchronized (Singleton.class) { if (null == uniqueSingleton) { uniqueSingleton = new Singleton(); } } } return uniqueSingleton; } }
为了解决上述问题,须要在uniqueSingleton
前加入关键字volatile
。使用了volatile关键字后,重排序被禁止,全部的写(write)操做都将发生在读(read)操做以前。
至此,双重检查锁就能够完美工做了。
参考资料: