单例模式要求一个类在全局范围内只能存在一个实例,同时要保证多线程环境下的实例唯一性。传统的单例实现方式比如双重检查锁、静态内部类,虽然能实现基本功能,但都存在被反射或者序列化破坏的风险,而枚举单例从JVM层面规避了这些问题,是目前公认最安全的单例实现方案。

传统单例的缺陷
我们先看常见的双重检查锁单例实现,这种方式虽然能实现延迟加载和线程安全,但存在明显问题:
public class DoubleCheckSingleton {
// 添加volatile保证可见性和禁止指令重排
private static volatile DoubleCheckSingleton instance;
private DoubleCheckSingleton() {}
public static DoubleCheckSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckSingleton.class) {
if (instance == null) {
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
这种实现有两个核心缺陷:第一,通过反射调用私有构造方法可以创建新的实例,破坏单例;第二,如果该类实现了Serializable接口,反序列化时会创建新的实例,同样破坏单例唯一性。
枚举单例的实现方式
枚举单例的实现非常简单,只需要定义一个枚举类,在其中声明一个枚举实例即可:
public enum EnumSingleton {
// 唯一的枚举实例,本身就是单例
INSTANCE;
// 可以添加自定义的业务方法
public void doSomething() {
System.out.println("执行枚举单例的业务逻辑");
}
}
使用时直接通过EnumSingleton.INSTANCE获取实例,调用业务方法:
public class Test {
public static void main(String[] args) {
EnumSingleton singleton1 = EnumSingleton.INSTANCE;
EnumSingleton singleton2 = EnumSingleton.INSTANCE;
// 输出true,证明两个引用指向同一个实例
System.out.println(singleton1 == singleton2);
singleton1.doSomething();
}
}
枚举单例安全的原因
JVM层面的实例唯一性
枚举类的实例在类加载阶段就会被初始化,JVM保证每个枚举常量在全局只有一个实例,而且枚举的初始化过程是线程安全的,不需要我们额外加同步锁。
天然防止反射攻击
Java的反射机制明确禁止通过反射创建枚举的实例,如果尝试用反射调用枚举的构造方法,会直接抛出IllegalArgumentException异常,从根源上避免了反射破坏单例的问题。
天然防止序列化破坏
枚举的序列化机制比较特殊,序列化时只会存储枚举的名称,反序列化时通过名称查找对应的枚举常量,不会创建新的实例,因此即使枚举单例实现了Serializable接口,也不会出现反序列化创建新实例的问题。
枚举单例的适用场景
枚举单例适合所有需要全局唯一实例的场景,比如配置管理器、线程池管理器、日志管理器等。不过如果单例需要继承其他类,枚举单例就不太适用,因为Java的枚举已经隐式继承了java.lang.Enum类,无法再继承其他类,这种场景下可以选择静态内部类单例,并做好反射和序列化的防护。
枚举单例的优缺点总结
| 优点 | 缺点 |
|---|---|
| 实现简单,代码量少 | 无法继承其他类 |
| JVM保证线程安全,无需额外同步 | 不支持延迟加载(枚举实例在类加载时初始化) |
| 天然防止反射和序列化破坏单例 | 如果不需要单例的场景强行使用会浪费资源 |
《Effective Java》中明确推荐优先使用枚举实现单例,这是实现单例的最佳实践。