反序列化会绕过构造器直接生成新对象,破坏单例唯一性;readresolve在对象构建完成后将其替换为已有实例,确保==一致。它要求类实现serializable、方法为private object readresolve()且返回已初始化的静态实例,但无法防御反射攻击。

反序列化会绕过构造器直接生成新对象,导致单例唯一性失效;readResolve 不阻止反序列化过程,而是在对象构建完成后将其替换成已有实例,从而守住 == 一致性。
为什么反序列化会破坏单例
Java 默认反序列化不调用任何构造方法、静态块或初始化逻辑,仅依据字节流填充字段。只要类实现 Serializable,ObjectInputStream 就能绕过所有防护,新建一个内存地址不同的对象。此时即使构造器里写了校验,也完全不生效——因为根本没走构造器。
常见后果包括:
- == 判断为 false
- hashCode() 不同
- 状态字段(如缓存、连接池)未共享,行为分裂
- 在 Redis 缓存、RPC 或消息队列场景中极易触发
readResolve 的正确写法和触发条件
这个方法不是可选补丁,而是 JVM 序列化规范定义的固定钩子,必须严格满足以下条件才会被自动调用:
- 所在类必须实现 Serializable 接口
- 方法声明必须是 private Object readResolve() throws ObjectStreamException
- 返回值必须是已初始化完成的静态单例实例(如 return INSTANCE; 或 return getInstance();)
- 不能是 public/protected/default —— 修饰符错就彻底失效
典型安全写法示例
推荐配合静态内部类初始化,确保 INSTANCE 在 readResolve 被调用前已就绪:
public class Singleton implements Serializable {private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
private Singleton() {
if (Holder.INSTANCE != null) throw new RuntimeException("Singleton already initialized");
}
public static Singleton getInstance() { return Holder.INSTANCE; }
private Object readResolve() { return getInstance(); }
}
关键点:
- INSTANCE 是 static final,由 JVM 类加载机制保证线程安全与提前初始化
- 构造器内加了非空校验,防御反射攻击(readResolve 对反射无效)
- readResolve 返回 getInstance(),复用同一入口,避免直接引用可能引发的 NPE
它能防什么,不能防什么
readResolve 是专为反序列化设计的“最后一道拦截”,效果明确但边界清晰:
- ✅ 防止反序列化产生新实例:无论序列化多少次,反序列化结果始终指向原始 INSTANCE
- ✅ 保持运行时状态一致:比如 private List
cache 字段,只要返回的是原实例,缓存就仍是那个已预热的对象 - ❌ 不防反射攻击:Constructor.setAccessible(true).newInstance() 仍可新建对象,必须靠构造器校验堵住
- ❌ 不防多 ClassLoader 场景:不同加载器下的同一个类,各自有独立 INSTANCE
如果项目允许且无需继承或懒加载延迟,枚举单例 public enum Singleton { INSTANCE; } 是更彻底的选择——JVM 层面硬编码保障,天然免疫反射、序列化、类加载器三重风险。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











