DCL不加volatile会因指令重排序导致对象未初始化完成就被引用,引发NPE或脏读;volatile禁止重排序并保证可见性;JDK5+才真正支持其内存语义。

为什么双重检查锁(DCL)不加 volatile 会出问题
因为 JVM 的指令重排序可能导致 instance 引用被提前赋值,而对象构造尚未完成。其他线程看到非 null 的 instance,却读到未初始化的字段,直接抛 NullPointerException 或返回脏数据。
-
volatile禁止对instance的读写重排序,并保证其构造过程对其他线程可见 - 仅对引用本身加
volatile即可,不需要整个类或构造逻辑加锁 - JDK 5+ 才真正支持
volatile的内存语义,老版本 JDK 下 DCL 是无效的
静态内部类方式比 synchronized 更轻量的原因
它利用了 Java 类加载机制的天然线程安全:类的初始化由 JVM 保证只执行一次,且是懒加载(第一次调用 SingletonHolder.INSTANCE 时才触发)。
- 没有同步块开销,无锁,无 CAS,无内存屏障,性能接近普通对象访问
- 不依赖
volatile或synchronized,也无需考虑构造器是否私有、是否防止反射攻击等额外防护 - 唯一限制是:必须是饿汉式语义(即首次访问才初始化),不能传参构造;若需初始化参数,得退回到 DCL +
volatile
枚举单例真的防不住反射和序列化吗
Java 枚举天生免疫反射构造(Enum 的构造器被 JVM 特殊保护)、序列化时也自动使用 readObject 的枚举专用逻辑,不会新建实例。
- 反射调用
Class.getDeclaredConstructor().newInstance()会直接抛java.lang.NoSuchMethodException - 反序列化时,即使你重写了
readResolve,枚举也不走这个流程——JVM 强制返回已有枚举常量 - 缺点是:无法继承、无法实现接口(除非接口方法在枚举中显式实现),且 IDE 可能提示“枚举不应用于单例”,属于风格争议而非技术缺陷
怎么选?看你的实际约束条件
没有银弹,关键看你在乎什么:是启动速度、并发吞吐、防御强度,还是代码可读性。
立即学习“Java免费学习笔记(深入)”;
- 要绝对安全 + 简洁 → 用
enum Singleton { INSTANCE; } - 要懒加载 + 高并发 + 兼容老 JDK → 用 DCL +
volatile(注意 JDK 版本) - 要懒加载 + 零同步开销 + 不需要构造参数 → 静态内部类最稳
- 别用
public static final Singleton INSTANCE = new Singleton();—— 它是饿汉式,但类加载时就初始化,可能浪费资源
最容易被忽略的是:单例对象自身的状态是否可变。哪怕创建过程线程安全,如果后续被多个线程并发修改内部字段,照样不是线程安全的单例。










