问题

单例对象在什么情况下可能被破坏?

答案

一、单例被破坏的四种主要场景

单例模式虽然保证了正常情况下只有一个实例,但在以下场景中可能被破坏:

  1. 反射攻击:通过反射调用私有构造函数
  2. 序列化破坏:反序列化时创建新实例
  3. 克隆破坏:通过 clone() 方法创建新实例
  4. 类加载器问题:不同类加载器加载同一个类

二、反射攻击及防御

攻击方式

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("新数据");

枚举单例的优势

  1. 防反射:JVM 层面禁止通过反射创建枚举实例
  2. 防序列化:枚举序列化由 JVM 保证,反序列化返回已有实例
  3. 防克隆:枚举不支持克隆
  4. 线程安全:类加载机制保证
  5. 代码简洁:无需手动编写防御代码

《Effective Java》作者 Joshua Bloch 的建议

“单元素的枚举类型已经成为实现单例的最佳方法。”

八、实际应用中的单例管理

Spring 容器中的单例

@Component
@Scope("singleton")  // 默认就是 singleton
public class UserService {
    // Spring 保证在同一个 ApplicationContext 中唯一
}

Spring 单例的特点

  • 容器级别的单例(不是 JVM 级别)
  • 线程安全(通过 ConcurrentHashMap 管理)
  • 支持懒加载(默认饿汉式,可配置懒加载)
  • 防止反射和序列化破坏(容器管理)

九、面试答题要点

  1. 四种破坏方式:反射、序列化、克隆、类加载器
  2. 反射防御:构造函数中检查 + 抛异常,或使用枚举
  3. 序列化防御:实现 readResolve() 方法,或使用枚举
  4. 克隆防御:不实现 Cloneable 或重写 clone() 抛异常
  5. 最佳方案:枚举单例(天然防御所有攻击)
  6. 实际应用:Spring 容器管理的单例 Bean
  7. 权衡选择:简单场景用静态内部类,需要防御攻击用枚举