dcl单例必须用volatile,因为jvm可能重排序对象初始化的三步(分配内存、初始化字段、赋值引用),导致其他线程获取到未构造完成的实例;volatile禁止重排序并保证可见性。

为什么 DCL 单例里必须用 volatile
因为不加 volatile,JVM 可能重排序对象初始化步骤,导致其他线程拿到未构造完成的实例。这不是理论风险——在 x86 以外的平台(比如 ARM)、或高并发压测下,getInstance() 真的会返回一个 new Singleton() 还没执行完构造函数的对象。
关键在于:JVM 允许将 memory = new Singleton() 拆成三步:
① 分配内存;② 初始化字段(含调用构造函数);③ 把引用赋值给静态变量 instance。
但②和③可能被重排序,而 volatile 禁止这种重排序,并保证后续读操作能看到完整的初始化结果。
- 只加
synchronized不加volatile—— 锁内安全,但锁外读取仍可能看到半初始化对象 -
volatile本身不保证原子性,但它提供了「写后读可见」+「禁止指令重排」两个必要保障 - Java 5+ 才真正修复了
volatile的语义,老版本(如 Java 1.4)无效
Java 中正确实现 DCL 单例的写法
核心是:静态变量声明带 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;
}
}
-
instance必须是static volatile,缺一不可 - 构造函数设为
private,防止反射绕过(反射另需防护,不在 DCL 范围内) - 不要在
getInstance()里做耗时操作(比如 IO),否则锁会拖慢所有线程 - 如果类有 final 字段,它们的初始化也受
volatile写的「发生前」保证,是安全的
DCL 在哪些场景下反而更危险
不是所有单例都适合 DCL。一旦对象初始化过程复杂,DCL 容易掩盖问题。
- 构造函数抛异常:
instance仍为null,下次调用又会尝试创建,但异常细节可能丢失 - 依赖外部状态(如配置、Spring 上下文):DCL 假设构造是幂等的,实际未必成立
- 子类继承单例类:若子类也实现 DCL,父类
instance和子类instance是两个变量,容易误用 - Android 中 Dalvik/ART 对 volatile 重排序行为曾有差异,低版本系统上偶发 crash
比 DCL 更简单且安全的替代方案
除非对启动性能极度敏感,否则直接用静态内部类或枚举更省心。
- 静态内部类方式:
SingletonHolder类只在getInstance()第一次调用时加载,天然线程安全,无重排序风险,且无同步开销 - 枚举方式:
public enum Singleton { INSTANCE; }—— JVM 保证枚举实例初始化绝对线程安全,还能防反射和反序列化攻击 - Spring 管理的
@Scope("singleton")Bean 默认就是单例,无需手写 DCL
真正需要 DCL 的场合其实很少:通常是底层工具类(如日志器、配置加载器),且明确要求懒加载 + 高并发 + 无框架依赖。其它情况,写错比写对更容易。






