dcl单例必须用volatile修饰实例字段,否则因重排序和可见性问题导致线程看到未初始化对象;volatile写与读构成happens-before关系,确保对象构造完成且对其他线程可见。

Java 中双重检查锁定(Double-Checked Locking, DCL)单例的正确实现,依赖 volatile 关键字来建立 happens-before 关系,从而禁止指令重排序并保证其他线程能看到完全初始化的对象。核心在于:没有 volatile,DCL 是不安全的;加上 volatile 后,JMM 通过 happens-before 规则保障了可见性与有序性。
volatile 写操作与读操作构成 happens-before
在 DCL 单例中,对 volatile 实例字段的写(如 instance = new Singleton())和后续任意线程对该字段的读(如 if (instance != null)),构成一个 happens-before 关系。这意味着:
- 写操作之前的所有内存写入(包括对象内部字段的初始化),对读操作所在的线程是可见的;
- JVM 不会将
new Singleton()的三步(分配内存、初始化对象、赋值引用)重排序为“分配→赋值→初始化”,从而避免其他线程看到未初始化完成的对象。
为什么不用 volatile 就会破坏 happens-before 链
若 instance 是普通变量,那么:
Java Maven 项目审查工作流的通用输入处理。当 Java Maven 项目以 ZIP压缩包或 GitLab 仓库 URL 形式提供且需要...时使用。
- 线程 A 在同步块内执行
instance = new Singleton(),其内部初始化动作可能被重排序到赋值之后; - 线程 B 在同步块外读到非 null 的
instance,但此时对象字段可能还未初始化(如仍为默认值 0、null); - 因为普通字段写与读之间不存在 JMM 定义的 happens-before 关系,B 线程无法保证看到 A 线程中构造器内的所有写操作结果。
happens-before 在 DCL 中的实际作用点
关键 happens-before 边发生在两个位置:
- 解锁操作(synchronized 块结束)→ volatile 写操作:由监视器锁规则保证(解锁 happens-before 后续对同一锁的加锁);但更直接的是 volatile 写本身自带的语义;
- volatile 写操作 → volatile 读操作:这是 volatile 变量规则明确规定的 happens-before 关系,它把对象构造过程中的所有写操作,“传递”给了后续读到该引用的线程。
标准写法回顾(体现 happens-before 的落地)
以下代码中,volatile 是唯一使 DCL 正确的关键:
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 写:触发 happens-before 效应
}
}
}
return instance; // volatile 读:能看见前面所有写
}
}
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










