Java序列化会绕过构造方法直接反射重建对象,破坏单例唯一性;解决方法是在类中添加private Object readResolve()方法返回唯一实例,或改用枚举单例。

Java 对象序列化会绕过构造方法,直接通过反射重建对象,导致反序列化后得到一个新实例,从而破坏单例的唯一性。解决的关键不是阻止反序列化,而是在反序列化完成后主动“替换”为原始单例实例。
为什么序列化会破坏单例
默认反序列化过程不调用任何构造器(包括私有构造器),而是由 JVM 底层分配内存并填充字段值。即使你用了双重检查锁、静态内部类等强保障写法,ObjectInputStream 仍会生成一个全新对象,其引用与 getInstance() 返回的对象不同。
- 反序列化对象与原单例对象地址不同,== 判断返回 false
- 该问题在所有实现 Serializable 接口的非枚举单例中普遍存在
- 哪怕构造器抛异常、加 synchronized,也完全不起作用
核心防御方案:readResolve 方法
在单例类中添加一个签名严格匹配的 private Object readResolve() 方法,JVM 在反序列化流程末尾自动调用它,并用其返回值覆盖刚创建的对象。
- 方法必须是 private Object readResolve(),不能带参数,不能是 static 或 public
- 返回值必须是当前单例的唯一实例,例如 return INSTANCE; 或 return getInstance();
- 该方法只对实现 Serializable 的类生效;若实现 Externalizable,则需在 readExternal 中手动处理
更彻底的解决方案:改用枚举单例
枚举类型由 JVM 规范强制保障单例性,天然免疫序列化和反射攻击,无需任何额外代码。
立即学习“Java免费学习笔记(深入)”;
- 反序列化时,ObjectInputStream 调用 readEnum(),直接从类的静态常量池中返回已有实例
- 反射调用 newInstance() 会直接抛出 NoSuchMethodException,无法创建新实例
- 写法简洁:public enum Singleton { INSTANCE; },获取方式为 Singleton.INSTANCE
使用 readResolve 时要注意的坑
看似加了方法就安全了,但以下情况会让它失效:
- 混淆工具(如 ProGuard)删掉了 readResolve 方法——需保留规则:-keep class * { private java.lang.Object readResolve(); }
- 使用 Kryo、Jackson 等第三方序列化库——默认不识别 readResolve,需显式启用或注册支持
- 类中存在 transient 字段或不可序列化成员(如 ThreadLocal)——readResolve 不负责恢复这些字段,需自行处理


















