单例模式的双重锁检查
一般的单例模式
1 | public class Singleton { |
但是在多线程情况下,可能会导致多个实例:
Time | Thread A | Thread B |
---|---|---|
T1 | 检查到uniqueSingleton 为空 |
|
T2 | 检查到uniqueSingleton 为空 |
|
T3 | 初始化对象A | |
T4 | 返回对象A | |
T5 | 初始化对象B | |
T6 | 返回对象B |
这种场景,就会创建两次对象。
加锁
代码如下:
1 | public class Singleton { |
这种写法也存在问题,加锁的粒度太大了,只有在创建对象时才需要加锁,后续获取对象时并不需要加锁。
双重锁检查
1 | public class Singleton { |
这样写,顺序就变成下面这样:
- 先判断对象是否存在,不存在则加锁。
- 加完锁之后再次判断对象是否存在。
- 不存在则创建。
双重加锁是因为如果多个线程同时通过了第一次判断,那么这几个线程中会有一个线程加锁成功,然后创建对象,后续线程在获取锁之后,就不用再继续创建对象。
存在的隐患
在实例化一个对象时,可以分为以下步骤:
- 分配内存空间
- 初始化对象
- 将对象指向刚分配的内存空间
但是编译器在为了执行速度,可能会进行指令重排序,那么顺序就有可能如下:
- 分配内存空间
- 将对象指向刚分配的内存空间
- 初始化对象
现在考虑如下场景:
Time | Thread A | Thread B |
---|---|---|
T1 | 检查到uniqueSingleton 为空 |
|
T2 | 获取锁 | |
T3 | 第二次检查uniqueSingleton 为空 |
|
T4 | 为uniqueSingleton 分配空间 |
|
T5 | 将uniqueSingleton 指向内存空间 |
|
T6 | 检查到uniqueSingleton 不为空 |
|
T7 | 访问uniqueSingleton (此时对象还未完成初始化) |
|
T8 | 初始化将uniqueSingleton |
这种情况下,线程B就访问到了一个还未初始化完成的对象。
解决办法
使用volatile
关键字禁止指令重排序。
1 | public class Singleton { |