volatile在双重检查锁中不保证原子性,而是确保instance变量的可见性和禁止指令重排序,防止线程看到未初始化完成的对象。

双重检查锁里 volatile 不是为原子性,而是防重排序
很多人误以为 volatile 在 DCL 中是用来“保证原子性”的,这是典型误解。DCL 的原子性靠的是 synchronized 块——它包裹了对象创建和赋值全过程,确保同一时刻只有一个线程能执行构造逻辑。而 volatile 根本不提供复合操作的原子性,volatile++ 依然线程不安全。
它的真正作用在别处:防止 JVM 或 CPU 将对象初始化的三步指令(分配内存 → 调用构造函数 → 赋值给静态引用)乱序执行。没有 volatile,第 3 步(instance = memory)可能被提前到第 2 步(ctorInstance(memory))之前,导致其他线程看到一个“已赋值但未初始化”的对象引用。
为什么 synchronized 不能完全替代 volatile?
synchronized 确实能保证临界区内的有序性,但它只约束块内代码;一旦线程 A 退出同步块,对 instance 的写入如果不加 volatile,就无法保证对线程 B 的“立即可见”+“顺序可见”。具体来说:
- 线程 A 在
synchronized块内完成instance = new Singleton(),但该写入可能滞留在 CPU 缓存中,未及时刷回主内存 - 线程 B 在块外读
instance != null,可能读到旧值(可见性问题) - 更危险的是:即使线程 B 侥幸读到了非 null 的
instance,这个引用指向的对象可能尚未完成构造(重排序导致)
volatile 写会插入 storeload 内存屏障,强制把前面所有写操作刷新到主内存;volatile 读则插入 loadload + loadstore 屏障,确保后续读写不会被提前到读之前——这正是 DCL 第二次检查能安全使用的底层保障。
不加 volatile 时典型的错误现象
这种 bug 极难复现,但真实存在。常见表现包括:
- 某个线程调用
getInstance()返回非 null 对象,但访问其字段时报NullPointerException(字段仍是默认值,如int为 0、引用为null) - 对象部分初始化成功,比如构造函数中前几行执行了,后几行没执行,状态不一致
- 仅在高并发、多核 CPU、JIT 优化开启等特定条件下触发,本地测试常“一切正常”
这不是 JVM Bug,而是 JMM(Java 内存模型)允许的行为——只要不加同步语义,重排序就是合法的优化。
volatile 在 DCL 中的最小必要写法
必须同时满足两个条件才有效:
- 修饰的是那个被双重检查的静态引用字段:
private static volatile Singleton instance; - 该字段只用于“发布”对象,不参与任何复合操作(如
instance.field++)
示例关键片段:
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 这里 new 的三步会被 volatile 约束顺序
}
}
}
return instance;
}
注意:如果把 volatile 错加在局部变量、方法参数或非发布字段上,完全无效;也别试图用 AtomicReference 替代——它虽有 volatile 语义,但 DCL 的语义依赖的是字段本身的 volatile 修饰,否则 JIT 仍可能重排构造过程。
最容易被忽略的一点:volatile 的禁止重排序效果,只作用于它所修饰的那个字段的读写操作及其前后指令。它不是全局开关,也不能“传染”给对象内部字段。所以对象自身的线程安全,还得靠设计保证。

















