dcl单例核心是兼顾线程安全与性能:必须用volatile修饰静态实例变量以禁止指令重排序、保证可见性;需两次判空检查(外层避免锁开销,内层防止多实例);私有构造器须防反射和序列化破坏。

直接用双重校验锁(DCL)实现单例,核心是兼顾线程安全与性能:只在首次创建实例时加锁,后续调用完全无同步开销。关键不在“能不能写出来”,而在于 volatile、两次判空、私有构造这三处细节缺一不可。
必须声明为 volatile 的静态实例变量
instance 变量不加 volatile,高并发下可能返回一个未初始化完成的对象。JVM 对 new 操作的三步(分配内存 → 调用构造器 → 赋值引用)可能重排序,导致其他线程看到非 null 但字段全为默认值的“半成品”对象。
- 正确写法:private static volatile Singleton instance;
- 不能省略 volatile —— 它同时保证内存可见性和禁止指令重排序
- 不要用 static final(那是饿汉式),也不要漏掉 static(否则不是类级单例)
两次判空检查不能少,顺序不能反
第一次检查在 synchronized 外,避免绝大多数调用进入锁;第二次检查在锁内,防止多个线程同时通过第一次检查后争抢创建。
- 第一次检查:if (instance == null) → 快速放行已初始化场景
- 进入 synchronized 块后,必须再次 if (instance == null) → 否则仍可能创建多个实例
- 两次检查之间不能插入任何可能改变 instance 的逻辑
私有构造器要防反射和序列化破坏
仅靠 private 构造器不足以真正保障唯一性。反射可绕过访问控制,反序列化可生成新对象。
- 构造器中加判断:if (instance != null) throw new RuntimeException("Singleton already initialized");
- 实现 readResolve() 方法,确保反序列化始终返回同一实例
- 如需彻底禁用反射,可在构造器中记录初始化状态并校验调用栈(较重,按需使用)
对比其他方式,明确 DCL 的适用边界
DCL 不是万能解,它适合需要懒加载 + 高并发 + 明确控制初始化时机的场景。若不满足这些,反而更推荐其他方案:
- 类加载即可用?→ 用饿汉式(简单、绝对线程安全、无 volatile 顾虑)
- 追求最简且 JDK ≥ 1.5?→ 用静态内部类(天然懒加载、线程安全、无同步开销)
- 要求枚举语义或防止反射?→ 用枚举单例(JVM 层保障,最安全)
- 只是临时全局配置?→ 考虑 Spring 管理的 @Scope("singleton"),而非手写










