volatile在双重检查锁单例中禁止指令重排序并保证内存可见性,防止线程拿到“半初始化”对象;synchronized仅保证临界区串行执行,无法阻止单线程内重排序导致的引用提前暴露。

因为不加 volatile,多线程下可能拿到一个“半初始化”的单例对象,导致程序崩溃或行为异常。
对象创建不是原子操作
看似简单的一行代码:
instance = new Singleton();
JVM 实际会拆成三步执行:
- 分配内存空间(memory = allocate())
- 调用构造方法初始化对象(ctorInstance(memory))
- 将引用赋值给 static 变量(instance = memory)
其中第 2 步和第 3 步在编译器或 CPU 层面**可能被重排序**,变成:1 → 3 → 2。也就是说,引用 instance 很早就被设为非 null,但对象内部字段还没初始化完成。
没有 volatile 时的危险场景
假设有两个线程 A 和 B 同时调用 getInstance():
- 线程 A 进入同步块,执行 new Singleton(),发生重排序后,刚做完第 3 步(instance 已非 null),但第 2 步(构造函数)还没跑完
- 此时线程 B 到达第一次检查:if (instance == null) → 判定为 false,直接返回这个 instance
- 线程 B 拿到的是一个“已分配内存、未初始化完成”的对象,访问其字段时可能为默认值(如 0、null),甚至抛出 NullPointerException 或逻辑错误
volatile 解决了什么
volatile 关键字在这里起两个关键作用:
- 禁止指令重排序:确保第 2 步(初始化)一定在第 3 步(赋值)之前完成,杜绝 1→3→2 的非法顺序
- 保证内存可见性:当线程 A 写入 instance,其他线程能立即看到最新值,不会因本地缓存读到过期的 null 或中间态
为什么 synchronized 不够用
锁只能保证**进入同步块的线程串行执行**,但它无法约束 JVM 或 CPU 对单个线程内指令的优化重排。一旦 instance 被写入(即使未初始化完),其他线程在锁外通过第一次检查就可能“抢读”到这个不安全的引用——而 volatile 正是为这种跨线程的“写-读”建立 happens-before 关系,堵住这个漏洞。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











