双重检查锁定拿不到完整对象是因为jvm/cpu指令重排序导致new操作三步被拆分,线程b可能看到未初始化的半成品;volatile通过内存屏障禁止初始化与赋值重排序,java 5+才真正支持;静态内部类、枚举单例更安全省心。

双重检查锁定为什么拿不到完整对象
因为 JVM 或 CPU 的指令重排序,让 new 对象的三步操作(分配内存 → 初始化 → 赋值给引用)被拆开,线程 B 可能在“分配内存 + 赋值引用”完成后就看到非 null 的实例,但此时初始化还没跑完——它拿到的是字段全为默认值(0 / null / false)的半成品。
volatile 是怎么堵住这个洞的
加 volatile 不是为了保证可见性那么简单,关键是它插入内存屏障(Memory Barrier),禁止编译器和处理器对 volatile 写操作之前的初始化指令做重排序。也就是说:instance = new Singleton() 这句里,“初始化”必须在“赋值给 instance”之前完成,且对其他线程可见。
常见错误写法:
private static Singleton instance; // ❌ 没 volatile,重排序风险仍在
正确写法:
private static volatile Singleton instance; // ✅ 必须加 volatile
- Java 5+ 才真正支持
volatile的禁止重排序语义;Java 4 及之前无效 -
synchronized块内部的赋值不用volatile也能安全,但双重检查的核心价值就是减少同步开销,所以外面那层判断必须依赖volatile - 别试图用
final字段替代——final能防止字段被修改,但不阻止构造函数没执行完就被发布
为什么静态内部类比双重检查更省心
因为类加载机制天然线程安全,且由 JVM 保证初始化完成才允许获取实例,完全绕开了手动控制指令序的问题。
典型写法:
private static class Holder {<br> private static final Singleton INSTANCE = new Singleton();<br>}
调用时直接 return Holder.INSTANCE;。它没有 volatile、没有 synchronized、没有重排序隐患,还能延迟加载。
- 适用场景:单例无参数、不依赖外部配置、构造过程不抛受检异常
- 如果构造函数可能抛
Exception,JVM 会把该类标记为“初始化失败”,后续所有访问都直接抛NoClassDefFoundError,而不是重试——这点比双重检查更难诊断 - Android 上某些低版本 Dalvik 对类加载顺序有特殊行为,极少数情况下会触发提前初始化(虽罕见,但线上出过)
现代 Java 里还值得手写双重检查吗
不推荐。除非你在维护一个不能升级到 Java 8+ 的老系统,或者明确需要构造时传参、做条件初始化等 Holder 搞不定的事。
- Lombok 的
@UtilityClass或 Spring 的@Bean+@Scope("singleton")更贴近实际工程需求 - 哪怕真要手写,也优先选枚举单例:
enum Singleton { INSTANCE; }——JVM 保证枚举实例的创建是原子且不可重排序的,连反射都拦得住 - 最容易被忽略的一点:双重检查只解决“多线程首次创建”的问题,不解决“创建后被修改状态”的并发问题——单例对象自身如果不是线程安全的,照样出错










