volatile的核心作用是阻止指令重排序,确保对象创建三步(分配内存、初始化、赋值引用)按序执行;否则可能暴露未初始化的“半成品”对象,导致空指针等错误。

在DCL单例中,volatile的核心作用是阻止对象创建过程中的指令重排序,确保其他线程看到的一定是完全初始化完毕的实例。
对象创建实际分三步执行
看似一行 instance = new Singleton(),JVM底层会拆解为三个独立操作:
- 分配内存空间(memory = allocate())
- 调用构造方法初始化对象(ctorInstance(memory))
- 将引用赋值给静态变量(instance = memory)
前两步不依赖第三步,编译器或CPU可能把第3步提前到第2步之前执行——即“先赋引用、后初始化”。这时 instance 已非 null,但对象内部字段尚未设置完成。
没有 volatile 时的风险场景
假设线程A正在执行初始化,恰好发生重排序:
- A 执行完第1步(分配内存)和第3步(instance 指向该内存),但还没执行第2步(构造函数)
- 此时 instance != null,线程B进入第一次检查,直接返回这个“半成品”对象
- B 调用该对象的方法时,可能读到字段默认值(如 int 字段为 0)、空引用或未初始化状态,引发 NPE 或逻辑错误
volatile 如何切断重排序链条
它在写操作(instance 赋值)后插入StoreStore屏障,强制要求:
- 构造函数执行必须在引用赋值之前完成
- 该写操作对其他线程立即可见(刷新到主内存)
- 读操作(if (instance == null))前插入LoadLoad屏障,保证能读到最新且完整的状态
这样就从JMM层面锁定了三步顺序:分配 → 初始化 → 赋值,杜绝了“引用先行”的中间态暴露。
为什么 synchronized 不足以解决这个问题
synchronized 只能保证临界区内的代码串行执行,但它无法约束临界区外的读操作与临界区内写操作之间的重排序关系:
- 第一次 null 判断(if (instance == null))不在 synchronized 块内,是并发执行的
- instance 的写入发生在锁内,但写入动作本身仍可能被重排
- volatile 提供的是跨线程、跨代码块的内存语义约束,弥补了锁的有序性盲区
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











