DCL单例模式设计为什么需要volatile修饰实例对象
DCL(Double-Checked Locking,双重检查锁)单例模式中,volatile 关键字对于保证 线程安全 和 正确的实例初始化 是 绝对必要 的。我们来系统地解析为什么 DCL 中必须使用 volatile 修饰单例实例对象。
一、DCL 单例模式的结构(不含 volatile)
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
这个实现目的是为了既避免重复创建实例,又能在第一次创建后避免每次访问都进入 synchronized,从而提高性能。
二、问题:为什么不加 volatile 会出错?
1. new Singleton() 不是原子操作
instance = new Singleton();
这行语句在 JVM 中会被拆分为 3 步操作(指令重排序前):
1)分配内存给对象
2)调用构造函数初始化对象
3)将对象引用赋值给变量(instance 指向这块内存)
但由于 JVM 允许指令重排序,实际执行可能变成这样:
1)分配内存
2)将对象引用赋值给变量
3)调用构造函数初始化对象
2. 重排序导致的问题:
如果线程 A 执行了重排序并在 step 2 赋值完后被线程 B 读取:
- 线程 B 发现
instance != null,于是跳过同步块,直接返回。 - 但此时
instance其实是一个 尚未初始化完成 的对象。 - 线程 B 访问该对象时可能引发 空指针异常 或其他未定义行为。
三、volatile 的作用:防止重排序 + 保证可见性
1. 禁止指令重排序
加上 volatile 后,instance = new Singleton(); 不允许发生 构造完成之前就赋值引用 的操作。
private static volatile Singleton instance;
JVM 层面会通过内存屏障指令(Memory Barrier)阻止指令重排序,确保初始化完成之后,才把对象引用赋值给 instance。
2. 保证线程间可见性
volatile能确保一个线程对instance的写入,对其他线程可见;- 没有
volatile,其他线程可能读到“旧值”或者未初始化值。
四、加上 volatile 的正确 DCL 实现
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
五、总结
| 原因 | 说明 |
|---|---|
| 指令重排序 | new Singleton() 会被重排序,可能返回未初始化对象 |
| 多线程可见性 | 没有 volatile,其他线程可能看不到最新值 |
volatile 的作用 | 禁止重排序,确保初始化安全,保证内存可见性 |
六、权威资料参考
- Java 官方文档 – volatile
- 《Java并发编程实战》 – 第 3、16 章
- JMM(Java 内存模型)官方文档
- Doug Lea 的《The Java Memory Model and Thread》
更多详细内容请关注其他相关文章!