在上篇博客中,已经提到 volatile 保证内存可见性的作用,这次加上 volatile 禁止指令重排序的作用。
class SingletonLazy {
private static volatile SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if(instance == null) {
synchronized (SingletonLazy.class) {
if(instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {
}
}
如果没有 volatile,这段代码在多线程环境下可能返回一个“未完全初始化”的对象,导致严重 bug!
🔍 为什么需要它?—— 深入分析“对象创建”过程
new SingletonLazy() 看似一行代码,但在 JVM 底层其实分为 3 步:
-
分配内存空间(为
SingletonLazy对象分配堆内存) -
调用构造方法(初始化对象的字段、执行构造逻辑)
-
将引用赋值给
instance(instance = 引用地址)
⚠️ 问题:JVM 可能重排序!
在没有 volatile 的情况下,JVM 或 CPU 可能将步骤 2 和 3 重排序,变成:
-
分配内存
-
将引用赋值给
instance(此时对象尚未初始化!) -
调用构造方法(延迟执行)
💥 这就是 “指令重排序”(Instruction Reordering) 优化。
🧨 多线程下的灾难场景(无 volatile)
假设有两个线程 T1 和 T2:
|
时间 |
T1(正在创建实例) |
T2(调用 |
|---|---|---|
|
1 |
执行 |
— |
|
2 |
— |
检查 |
|
3 |
— |
直接 return instance(拿到一个未初始化的对象!) |
|
4 |
② 执行构造方法(太晚了!) |
使用 |
❌ 这就是著名的 “DCL 失效问题”(在 Java 5 之前无法安全修复)。
✅ volatile 如何解决这个问题?
当你声明:
private static volatile SingletonLazy instance = null;
volatile 提供两个关键保障:
1. 禁止重排序
-
JVM 保证
instance = new SingletonLazy()的三步操作不会被重排序。 -
必须严格按照:分配内存 → 初始化 → 赋值 的顺序执行。
2. 保证可见性
-
当 T1 执行
instance = ...后,T2 立即能看到最新值(不会读到缓存中的null)。
✅ 结果:T2 要么看到
instance == null(继续等待锁),
要么看到一个完全初始化好的对象。
❌ 如果去掉 volatile 会怎样?
private static SingletonLazy instance = null; // 没有 volatile!
-
单线程:一切正常。
-
多线程:有极小概率(但确实存在)返回未初始化对象。
-
Bug 难以复现(依赖 CPU/JVM/时机),但一旦发生,后果严重(崩溃、数据错乱)。
🐞 这类 bug 被称为 “Heisenbug”(观测时消失,不观测时出现)。
763

被折叠的 条评论
为什么被折叠?



