dcl实现单例必须用volatile,因其禁止指令重排序,防止“分配内存→引用赋值→初始化对象”的错误顺序导致其他线程获取未初始化完成的半成品实例;外层if避免不必要的同步,内层if防止多线程重复创建。

用双重检查锁(DCL)实现单例,核心是“两次判空 + 同步块 + volatile 修饰”,既保证线程安全,又避免每次调用都加锁的性能损耗。关键不在“有没有锁”,而在于“什么时候锁、锁多长、怎么防重排”。
为什么必须用 volatile?
volatile 不是用来加锁的,而是解决 JVM 指令重排序带来的隐患:
- 对象创建实际分三步:分配内存 → 初始化对象 → 将引用指向该内存
- 若没 volatile,JVM 可能重排为:分配内存 → 引用赋值 → 初始化对象
- 此时另一个线程看到引用不为 null,但对象尚未初始化完成,就会拿到一个半成品实例
- volatile 禁止重排,同时保证写操作对其他线程立即可见
两次判空各自起什么作用?
第一次判空在同步块外,第二次在同步块内,分工明确:
- 外层 if:快速路。实例已存在就直接返回,完全绕过锁,高并发下绝大多数调用走这条路
- 内层 if:保险锁。只有真正需要创建时才进同步块,且进入后再次确认——防止多个线程排队等锁期间,前一个已创建完,后一个又重复创建
锁对象选什么?怎么写才不出错?
锁对象必须是类级别、全局唯一、不可变的引用:
- Java 推荐用 类字面量(如
Singleton.class),不是this或任意 new 出来的对象 - Go 中用
sync.Mutex配合包级变量,注意 不能用 defer 解锁——因为锁是在第二次判空后才获取的,defer 会等到函数末尾才执行,导致锁持有时间过长甚至死锁 - 解锁必须紧跟在第二次判空之后、return 之前显式调用
比起 DCL,为什么更推荐 sync.Once(Go)或静态内部类(Java)?
不是 DCL 有缺陷,而是它容易写错,而替代方案更简洁、更难出错:
-
sync.Once内部已封装原子操作与锁,调用once.Do()即可,无需手动管理 volatile、synchronized、双重判断 - Java 静态内部类利用 JVM 类加载机制的天然线程安全,延迟加载且无同步开销,代码更短、语义更清晰
- DCL 是“可控但易错”,适合理解底层原理;生产环境优先选更稳妥的封装方案











