懒汉式单例不加volatile会返回未初始化对象,根本原因是new Singleton()被拆分为分配内存、初始化、赋值三步,无volatile时JVM/CPU可能重排序后两步,导致引用提前写入而对象未初始化。

为什么懒汉式单例不加 volatile 会返回未初始化对象
根本原因在于 new Singleton() 不是原子操作,它被 JVM 拆成三步:分配内存 → 调用构造方法初始化 → 将引用赋值给静态变量。若无 volatile,JVM 或 CPU 可能将第二、三步重排序,导致引用提前写入,而对象尚未完成初始化。此时另一个线程看到 instance != null 就直接返回,调用其方法时触发 NullPointerException。
双重检查锁(DCL)中 volatile 的正确写法
必须同时满足两个条件才安全:
-
instance字段必须用volatile修饰(仅修饰字段本身,不是方法或局部变量) - 同步块内必须再次判空,且只在内部执行
new Singleton()
典型错误是漏掉 volatile,或把 synchronized 加在方法上(失去 DCL 意义),或在同步块外就 new 实例。
正确示例:
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;
}
}
volatile 在这里只管重排序,不管原子性
很多人误以为加了 volatile 就能替代锁 —— 实际不能。它只保证两点:
- 可见性:一个线程对
instance的写,对其他线程立即可见 - 禁止重排序:阻止
instance = new Singleton()中的「赋值」与「初始化」乱序
但它不保证 if (instance == null) 和 instance = new Singleton() 这两步合起来是原子的,所以仍需 synchronized 块来串行化创建逻辑。
容易忽略的兼容性细节
Java 5 及以后版本才正式支持 volatile 的禁止重排序语义;Java 4 及更早版本即使写了 volatile 也无法阻止该问题。如果你的项目还在用 JDK 1.4,DCL + volatile 是无效的,只能退回到全方法同步或饿汉式。
另外,volatile 对 long/double 类型的读写提供原子性保障(JMM 规定),但这和单例无关,别混淆。
真正关键的那条线,是 volatile 修饰的字段必须出现在 new 表达式的左侧,且不能被任何非 volatile 写操作干扰 —— 否则屏障失效,重排序风险照旧。

















