问题
单例对象在什么情况下可能被破坏?
答案
一、单例被破坏的四种主要场景
单例模式虽然保证了正常情况下只有一个实例,但在以下场景中可能被破坏:
- 反射攻击:通过反射调用私有构造函数
- 序列化破坏:反序列化时创建新实例
- 克隆破坏:通过
clone()方法创建新实例 - 类加载器问题:不同类加载器加载同一个类
二、反射攻击及防御
攻击方式
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
// 私有构造函数
}
public static Singleton getInstance() {
return INSTANCE;
}
}
// 反射攻击
public class ReflectionAttack {
public static void main(String[] args) throws Exception {
// 正常获取单例
Singleton instance1 = Singleton.getInstance();
// 通过反射破坏单例
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true); // 绕过私有访问控制
Singleton instance2 = constructor.newInstance();
System.out.println(instance1 == instance2); // false,单例被破坏
}
}
防御措施
方法1:在构造函数中检查
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private static boolean initialized = false;
private Singleton() {
synchronized (Singleton.class) {
if (initialized) {
throw new RuntimeException("单例已存在,禁止通过反射创建");
}
initialized = true;
}
}
public static Singleton getInstance() {
return INSTANCE;
}
}
方法2:使用枚举(最安全)
public enum Singleton {
INSTANCE;
public void doSomething() {
// 业务方法
}
}
// 枚举天然防止反射攻击
// 反射调用 newInstance() 时会抛出 IllegalArgumentException
原理:
// Constructor.newInstance() 源码片段
if ((clazz.getModifiers() & Modifier.ENUM) != 0) {
throw new IllegalArgumentException("Cannot reflectively create enum objects");
}
三、序列化破坏及防御
攻击方式
public class Singleton implements Serializable {
private static final Singleton INSTANCE = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return INSTANCE;
}
}
// 序列化攻击
public class SerializationAttack {
public static void main(String[] args) throws Exception {
Singleton instance1 = Singleton.getInstance();
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
oos.writeObject(instance1);
oos.close();
// 反序列化(创建新实例)
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser"));
Singleton instance2 = (Singleton) ois.readObject();
ois.close();
System.out.println(instance1 == instance2); // false,单例被破坏
}
}
防御措施
方法1:实现 readResolve() 方法
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static final Singleton INSTANCE = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return INSTANCE;
}
// 反序列化时返回已有实例
private Object readResolve() {
return INSTANCE;
}
}
原理:
// ObjectInputStream.readObject() 源码片段
if (desc.hasReadResolveMethod()) {
Object rep = desc.invokeReadResolve(obj);
if (rep != obj) {
obj = rep; // 使用 readResolve() 返回的对象
}
}
方法2:使用枚举(推荐)
public enum Singleton implements Serializable {
INSTANCE;
public void doSomething() {
// 业务方法
}
}
// 枚举的序列化由 JVM 保证,天然防止序列化破坏
枚举序列化原理:
// 枚举序列化时只写入 name
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeUTF(name());
}
// 反序列化时通过 name 查找已有实例
private void readObject(ObjectInputStream in) throws IOException {
String name = in.readUTF();
return Enum.valueOf(enumClass, name); // 返回已有实例
}
四、克隆破坏及防御
攻击方式
public class Singleton implements Cloneable {
private static final Singleton INSTANCE = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return INSTANCE;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 创建新实例
}
}
// 克隆攻击
public class CloneAttack {
public static void main(String[] args) throws Exception {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = (Singleton) instance1.clone();
System.out.println(instance1 == instance2); // false,单例被破坏
}
}
防御措施
方法1:不实现 Cloneable 接口
public class Singleton {
// 不实现 Cloneable 接口
// 调用 clone() 会抛出 CloneNotSupportedException
}
方法2:重写 clone() 方法抛出异常
public class Singleton implements Cloneable {
private static final Singleton INSTANCE = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return INSTANCE;
}
@Override
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException("单例对象不允许克隆");
}
}
方法3:重写 clone() 返回已有实例
@Override
protected Object clone() throws CloneNotSupportedException {
return INSTANCE; // 返回已有实例
}
五、类加载器问题
问题场景
// 不同类加载器加载同一个类,会创建不同的 Class 对象
ClassLoader loader1 = new CustomClassLoader();
ClassLoader loader2 = new CustomClassLoader();
Class<?> class1 = loader1.loadClass("com.example.Singleton");
Class<?> class2 = loader2.loadClass("com.example.Singleton");
Object instance1 = class1.getMethod("getInstance").invoke(null);
Object instance2 = class2.getMethod("getInstance").invoke(null);
System.out.println(instance1 == instance2); // false
防御措施
使用同一个类加载器:
// 在 Web 应用中,确保单例类由应用类加载器加载
// 避免放在共享库中被不同类加载器加载
使用容器管理:
// Spring 容器保证单例 Bean 在同一个 ApplicationContext 中唯一
@Component
public class Singleton {
// Spring 管理的单例
}
六、完整的防御方案对比
| 破坏方式 | 饿汉式 | 懒汉式(DCL) | 静态内部类 | 枚举 |
|---|---|---|---|---|
| 反射攻击 | ❌ 需手动防御 | ❌ 需手动防御 | ❌ 需手动防御 | ✅ 天然防御 |
| 序列化破坏 | ❌ 需实现 readResolve | ❌ 需实现 readResolve | ❌ 需实现 readResolve | ✅ 天然防御 |
| 克隆破坏 | ❌ 需重写 clone | ❌ 需重写 clone | ❌ 需重写 clone | ✅ 天然防御 |
| 线程安全 | ✅ 天然安全 | ✅ volatile + DCL | ✅ 类加载保证 | ✅ 天然安全 |
| 懒加载 | ❌ 不支持 | ✅ 支持 | ✅ 支持 | ❌ 不支持 |
七、最佳实践:枚举单例
public enum Singleton {
INSTANCE;
// 实例变量
private String data;
// 构造函数(可选)
Singleton() {
this.data = "初始化数据";
}
// 业务方法
public void doSomething() {
System.out.println("执行业务逻辑: " + data);
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
// 使用
Singleton.INSTANCE.doSomething();
Singleton.INSTANCE.setData("新数据");
枚举单例的优势:
- 防反射:JVM 层面禁止通过反射创建枚举实例
- 防序列化:枚举序列化由 JVM 保证,反序列化返回已有实例
- 防克隆:枚举不支持克隆
- 线程安全:类加载机制保证
- 代码简洁:无需手动编写防御代码
《Effective Java》作者 Joshua Bloch 的建议:
“单元素的枚举类型已经成为实现单例的最佳方法。”
八、实际应用中的单例管理
Spring 容器中的单例
@Component
@Scope("singleton") // 默认就是 singleton
public class UserService {
// Spring 保证在同一个 ApplicationContext 中唯一
}
Spring 单例的特点:
- 容器级别的单例(不是 JVM 级别)
- 线程安全(通过 ConcurrentHashMap 管理)
- 支持懒加载(默认饿汉式,可配置懒加载)
- 防止反射和序列化破坏(容器管理)
九、面试答题要点
- 四种破坏方式:反射、序列化、克隆、类加载器
- 反射防御:构造函数中检查 + 抛异常,或使用枚举
- 序列化防御:实现
readResolve()方法,或使用枚举 - 克隆防御:不实现
Cloneable或重写clone()抛异常 - 最佳方案:枚举单例(天然防御所有攻击)
- 实际应用:Spring 容器管理的单例 Bean
- 权衡选择:简单场景用静态内部类,需要防御攻击用枚举