指令重排序是单例线程不安全的核心原因:new操作的三步(分配、初始化、赋值)可能被重排为“分配+赋值→初始化”,导致其他线程拿到未初始化完成的对象;volatile通过禁止重排序和保证可见性修复DCL,而饿汉式因JVM类加载机制天然规避该问题。

指令重排序本身不破坏单线程逻辑,但在多线程环境下,它会让看似安全的懒汉式单例(尤其是双重检查锁 DCL)产生“构造未完成就暴露引用”的问题——这是单例模式线程不安全的核心原因之一。
单例对象创建过程其实分三步
以 new Singleton() 为例,JVM 实际执行包含三个原子操作:
- 在堆中分配内存空间
- 调用构造方法初始化对象(即真正设置字段值)
- 将对象引用赋值给静态变量
instance
编译器和处理器可能把第2步和第3步重排序:先完成内存分配+赋值引用,再执行构造初始化。此时 instance != null 已为真,但对象内部字段可能还是默认值(如 0、null),其他线程拿到这个“半初始化”实例就会出错。
DCL 中的重排序陷阱
下面这段经典代码看似加了同步、做了两次判空,仍可能出问题:
立即学习“Java免费学习笔记(深入)”;
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // ← 这里可能发生重排序!
}
}
}
return instance;
}
问题就出在 instance = new Singleton() 这一行:若未加 volatile,JVM 允许把“赋值引用”提前到“构造完成”之前。线程A执行到这里并写入 instance,线程B恰好在此时进入第一个 if 判断,发现 instance != null 就直接返回——但该对象尚未初始化完毕。
volatile 是关键修复手段
给 instance 加上 volatile 修饰后:
- 禁止对它的写操作与前面的操作重排序(防止“赋值引用”跑到构造之前)
- 禁止对它的读操作与后面的操作重排序(确保读到的是完全初始化后的对象)
- 同时保证可见性:一个线程写完,其他线程能立即看到最新值
这样就能确保:只要 instance != null,它一定是构造完成、状态一致的对象。
饿汉式为什么天然规避重排序问题
饿汉式在类加载阶段就完成实例化:
private static final Singleton instance = new Singleton();
类初始化由 JVM 保证是线程安全的,且整个过程发生在静态初始化块中,受 JVM 内置的初始化锁保护;更重要的是,static final 字段的赋值具有“初始化完成才可见”的语义,天然具备 happens-before 关系,不需要额外加 volatile 或同步。

















