饿汉式线程安全因类加载时初始化且JVM保证static变量仅执行一次,但会提前创建实例导致资源浪费;DCL需volatile防止重排序和可见性问题;静态内部类利用类加载机制实现延迟加载与线程安全;枚举单例天然防反射和反序列化攻击。

饿汉式单例为什么线程安全但可能浪费资源
饿汉式在类加载时就初始化实例,static 变量由 JVM 保证只执行一次,天然线程安全。但它的问题是:无论后续是否调用 getInstance(),实例都会被创建。
- 适合实例创建成本低、且应用启动后必然使用的场景
- 如果单例依赖外部配置或数据库连接,而这些资源在启动时不可用,就会直接抛出
ExceptionInInitializerError - 不能实现延迟加载,对内存敏感的应用需谨慎
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return instance;
}
}
双重检查锁(DCL)必须加 volatile 才可靠
不加 volatile 会导致指令重排序,一个线程可能看到未完全构造的实例——这是 DCL 最经典也最容易被忽略的坑。
-
volatile禁止重排序,并保证可见性 - 第一次判空(避免同步开销),第二次判空(防止重复初始化)
- JDK 1.5+ 才真正支持
volatile的语义,旧版本仍不安全
public class DclSingleton {
private static volatile DclSingleton instance;
private DclSingleton() {}
public static DclSingleton getInstance() {
if (instance == null) {
synchronized (DclSingleton.class) {
if (instance == null) {
instance = new DclSingleton();
}
}
}
return instance;
}
}
静态内部类方式兼顾延迟加载与线程安全
利用 JVM 类加载机制:外部类加载时,静态内部类不加载;只有首次调用 getInstance() 触发内部类加载,才初始化实例。这个过程由 JVM 保证原子性。
- 无需同步、无
volatile、无重排序风险 - 比 DCL 更简洁,且 JDK 1.2+ 即可安全使用
- 不能用于需要传参构造的场景(因为构造发生在类加载期,无法动态传参)
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {}
private static class Holder {
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance() {
return Holder.INSTANCE;
}
}
枚举单例防反射和反序列化攻击
反射调用私有构造器、反序列化绕过 getInstance() 是其他实现方式共有的漏洞。枚举类型由 JVM 保障全局唯一,且 java.lang.Enum 的构造逻辑禁止反射创建新实例。
立即学习“Java免费学习笔记(深入)”;
- 写法最简,天然序列化安全
- 若单例需实现接口,可让枚举 implements 该接口
- 不能继承其他类(Java 枚举隐式继承
Enum),扩展性受限
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
// 实际逻辑
}
}
枚举方式看似简单,但它真正解决的是其他实现难以彻底规避的安全边界问题——比如你写了完美的 DCL,却忘了在构造器里加反射校验,或者没重写 readResolve(),就可能被绕过。这点在分布式或高权限环境中尤其关键。










