一、为什么需要枚举单例?
先看传统单例(如饿汉式)的"软肋"------反射攻击:
java
public class HungrySingleton {
private static final HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton() {} // 私有构造器
public static HungrySingleton getInstance() {
return INSTANCE;
}
}
// 反射可以强行破坏单例!
Constructor<HungrySingleton> constructor = HungrySingleton.class.getDeclaredConstructor();
constructor.setAccessible(true); // 暴力破解private
HungrySingleton evil = constructor.newInstance(); // 创建了第二个实例!
System.out.println(evil == HungrySingleton.getInstance()); // false --- 单例被破坏
传统的 private 构造器根本挡不住反射。而枚举单例天然免疫反射攻击。
二、枚举单例的写法
写法极其简洁:
java
public enum Singleton {
INSTANCE;
// 可以添加任意属性和方法
private String name;
private int age;
// 枚举的构造方法(默认就是private,不能手动加修饰符)
Singleton() {
this.name = "default";
this.age = 0;
}
// 业务方法
public void doSomething() {
System.out.println("Singleton is working...");
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
使用方式:
java
Singleton s1 = Singleton.INSTANCE;
Singleton s2 = Singleton.INSTANCE;
System.out.println(s1 == s2); // true,始终是同一个实例
s1.doSomething();
s1.setName("MiMo");
System.out.println(s1.getName()); // MiMo
三、为什么枚举能实现单例?------JVM 层面的保障
1. 类加载机制保证唯一实例
枚举常量(INSTANCE)在枚举类被加载时,由 JVM 保证只实例化一次。它本质上等价于:
java
public static final Singleton INSTANCE = new Singleton();
但比手动写饿汉式更可靠,因为这是 JVM 规范强制保证的。
2. 反射攻击被 JVM 阻止
java
// 试图反射创建枚举实例 ------ 会失败!
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
Singleton evil = constructor.newInstance("evil", -1);
运行结果:
java
Exception in thread "main" java.lang.IllegalArgumentException:
Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
JVM 在 Constructor.newInstance() 中做了硬编码检查:
java
// JDK源码 java.lang.reflect.Constructor#newInstance
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
这是 JVM 级别的防护,不是语言层面的,所以无法绕过。
3. 序列化安全
传统单例在序列化/反序列化时会创建新对象,需要重写 readResolve()。而枚举天然防序列化破坏:
java
// 传统单例需要这样防御:
private Object readResolve() {
return INSTANCE; // 忘了写就破功
}
// 枚举单例:什么都不用写,JVM自动保证反序列化后还是同一个实例
JVM 对枚举的反序列化做了特殊处理,直接返回已有的枚举常量,而不是通过 new 创建对象。
4. 线程安全
枚举常量的初始化由 JVM 类加载机制完成,而 JVM 的类加载过程是天然线程安全 的(由 ClassLoader 的初始化锁保证)。无需 synchronized,无需 volatile,无需双重检查。
5. 完整实战示例
java
public enum DatabaseConnection {
INSTANCE;
private final String url;
private final String username;
private final String password;
private boolean connected;
// 枚举构造方法 ------ JVM保证只执行一次
DatabaseConnection() {
this.url = "jdbc:mysql://localhost:3306/mydb";
this.username = "root";
this.password = "secret";
this.connected = false;
System.out.println("数据库连接池初始化...(只会出现一次)");
}
public synchronized void connect() {
if (!connected) {
System.out.println("连接到: " + url);
connected = true;
} else {
System.out.println("已经连接过了");
}
}
public synchronized void disconnect() {
if (connected) {
System.out.println("断开连接");
connected = false;
}
}
public String getUrl() { return url; }
}
// 使用
public class Main {
public static void main(String[] args) {
DatabaseConnection.INSTANCE.connect();
DatabaseConnection.INSTANCE.connect(); // "已经连接过了"
DatabaseConnection.INSTANCE.disconnect();
// 验证单例
DatabaseConnection a = DatabaseConnection.INSTANCE;
DatabaseConnection b = DatabaseConnection.INSTANCE;
System.out.println(a == b); // true
}
}
四、总结对比
| 特性 | 饿汉/懒汉式 | 双重检查锁 | 静态内部类 | 枚举 |
|---|---|---|---|---|
| 写法简洁度 | 一般 | 复杂 | 一般 | 最简洁 |
| 线程安全 | 需处理 | 需volatile | 天然安全 | 天然安全 |
| 反射攻击 | 不防 | 不防 | 不防 | JVM级别防御 |
| 序列化安全 | 需readResolve | 需readResolve | 需readResolve | 天然安全 |
| 延迟加载 | 视写法 | 支持 | 支持 | 不支持(饿汉) |
| 可继承性 | 可继承 | 可继承 | 可继承 | 不可继承 |
枚举单例唯一的缺点:不支持延迟加载,且枚举类不能被继承。但对于绝大多数场景,这是最安全、最简洁的单例实现方式------正如《Effective Java》Item 3 所说:
"A single-element enum type is often the best way to implement a singleton."