volatile在dcl单例中是必需的,它通过禁止指令重排序(确保new的三步不被乱序)和保证可见性(强制主内存读写),防止线程读到半初始化对象,并建立happens-before关系使构造完成状态对所有线程立即可见。

在双重检查锁定(DCL)单例中,volatile 是必须加的,不是可选优化。它不参与加锁,但专门堵住 JVM 和 CPU 指令重排带来的“半初始化对象”漏洞——这是 DCL 能安全落地的底线保障。
volatile 必须修饰静态实例变量
声明方式只有一种正确写法:
- private static volatile Singleton instance; ✅
- 不能漏掉 static(否则不是类级单例)
- 不能漏掉 volatile(否则无法禁止重排、无法保证可见)
- 不能用
synchronized替代它——锁管不了构造完成前的赋值可见性
理解重排漏洞:new Singleton() 实际分三步
看似一行代码 instance = new Singleton(),JVM 执行时实际拆成:
- 1. 在堆中分配内存空间
- 2. 调用构造方法初始化对象(可能含字段赋值、资源加载等)
- 3. 将引用赋值给
instance变量
若无 volatile,步骤 2 和 3 可能被重排为「1→3→2」。此时另一个线程执行外层 if (instance != null) 会直接拿到一个指向未初始化内存的引用,调用其方法大概率抛 NullPointerException 或读到默认值(如 int 字段为 0 而非预期值)。
volatile 如何堵住这个漏洞
它通过两层机制协同生效:
-
禁止重排序:在
instance = new Singleton()的写操作前后插入内存屏障,强制步骤 2 完成后再执行步骤 3 -
保证可见性:每次读
instance都从主内存加载,每次写都立即刷回主内存,避免线程从本地缓存读到旧值或部分更新值 - 建立 happens-before 关系:写 volatile 变量的操作,happens-before 任意后续对该变量的读操作——这意味着读线程一定能看见构造函数内所有已完成的初始化动作
完整正确写法示例
注意:构造函数私有、两次判空、同步块内二次检查,缺一不可;volatile 是最后也是最关键的一环:
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(); // volatile 保证这行执行完,对象才真正“发布”
}
}
}
return instance;
}
}Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











