static字段+私有构造非万能单例,因类加载即初始化且多线程下可能创建多个实例;懒汉式需synchronized但性能差;dcl需volatile防重排序;枚举最安全但无法延迟加载、不支持继承,且反序列化不恢复可变字段状态。

为什么 static 字段 + 私有构造不是万能的单例
因为类加载时就初始化,不管用不用都占内存;而且多线程下可能创建多个实例。比如两个线程同时执行到 instance = new Singleton(),JVM 不保证原子性,instance 可能被赋值两次。
- 懒汉式必须加
synchronized,但锁整个方法会严重拖慢性能 - 双重检查锁定(DCL)要加
volatile修饰instance,否则可能因指令重排序看到未构造完成的对象 - 枚举实现最安全,但无法继承、不能延迟加载,且有些框架(如某些序列化工具)对枚举单例支持不一致
Enum 单例在反序列化时真的一劳永逸吗
Java 枚举天生防反射、防序列化绕过,readObject 被 JVM 特殊处理,始终返回同一个实例。但注意:如果单例类里持有非 transient 的可变状态(比如一个 HashMap),反序列化后该状态不会自动恢复 —— 枚举只保实例身份,不保字段内容。
- 不要在枚举单例中存可变对象引用,或确保它们本身也是不可变的
- 若必须保存状态,得手动实现
readResolve(),但枚举类不允许重写该方法 - Spring 等容器管理的 Bean 默认不是枚举,别把
@Component和枚举单例混用
Spring 中的 @Scope("singleton") 和手写单例是一回事吗
不是。Spring 的 singleton 是“容器内单例”,即每个 ApplicationContext 中只创建一次 Bean 实例;而手写单例是 JVM 级别的类单例,跨 ClassLoader 都唯一(除非打破双亲委派)。
- 同一应用里启动两个
AnnotationConfigApplicationContext,@Scope("singleton")的 Bean 会有两份 - 手写单例(如饿汉式)在不同 ClassLoader 加载时,也会出现多个实例
- Spring Boot 默认 Web 应用只有一个上下文,所以通常表现像单例,但本质依赖容器生命周期
用 java.util.concurrent.atomic.AtomicReference 实现无锁单例靠谱吗
可以,但容易误用。它适合“首次设置后不再变更”的场景,比如配置加载器;但无法解决构造过程本身的线程安全问题 —— AtomicReference.compareAndSet(null, new Singleton()) 中的 new Singleton() 仍可能被多次执行。
立即学习“Java免费学习笔记(深入)”;
- 正确做法是先构造好实例(静态块或 static final),再用
AtomicReference包装,否则没意义 - 比起 DCL 或枚举,这种写法增加了理解成本,却没带来实际优势
- 如果单例初始化耗时且需失败重试,
AtomicReference搭配 CAS 循环更合适,但得小心无限重试或状态污染
真正难的不是写出“看起来只创建一次”的代码,而是想清楚“谁来控制生命周期”“在哪一层保证唯一”“出错时怎么兜底”。这些决定比选哪种写法更重要。










