readResolve通过在反序列化末尾替换新对象为已有静态实例来保障单例唯一性;需实现Serializable、签名严格为private Object readResolve()、返回已初始化的唯一实例,但无法防御反射攻击。

Java 中通过 readResolve 保护单例,核心是在反序列化完成的“最后一刻”,把 JVM 刚新建的对象替换成早已存在的那个唯一实例——这样无论序列化多少次,反序列化出来的对象始终和原单例是同一个内存地址(== 为 true)。
为什么反序列化会破坏单例
Java 反序列化不调用构造器,而是直接反射分配内存、填充字段,相当于绕过所有单例控制逻辑(比如私有构造校验、静态内部类初始化等),硬生生造出一个新对象。哪怕你用了最严谨的静态内部类写法,这个新对象仍会独立存在,导致单例失效。
readResolve 的作用机制
它不是阻止反序列化,而是在反序列化流程结束前被 JVM 自动调用,用你指定的返回值覆盖刚创建的对象:
- 方法签名必须严格为
private Object readResolve() throws ObjectStreamException - 返回值必须是那个已初始化好的单例实例,例如
return INSTANCE;或return getInstance(); - 修饰符只能是
private,不能是public、protected或包级可见 - 所在类必须实现
Serializable接口,否则 JVM 根本不会查找该方法
正确写法的关键细节
光加方法还不够,得确保它真正生效:
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
立即学习“Java免费学习笔记(深入)”;
- 推荐用静态内部类方式初始化单例,保证
INSTANCE在readResolve被调用前已完成加载 - 构造器里加非空判断(如
if (Holder.INSTANCE != null) throw new RuntimeException(...)),防反射攻击(readResolve本身对此无效) - 避免使用
Externalizable接口——它会完全跳过readResolve,需手动在readExternal中返回单例 - 字段含
transient或不可序列化类型(如ThreadLocal)时,这些字段反序列化后为null,readResolve不负责恢复它们
它能防什么、不能防什么
能防:反序列化产生的新实例;集合类等引用字段的语义一致性(比如缓存列表仍是那个预热过的对象)。
不能防:反射调用私有构造器(需配合构造器内校验);不同类加载器加载同一类导致的多个 INSTANCE;某些第三方序列化库(如 Kryo、Jackson)默认不识别 readResolve,需显式启用支持。
如果单例不需要继承或实现复杂接口,直接用枚举是最省心、最彻底的方案——JVM 层面禁止重建枚举实例,天然免疫反序列化与反射攻击。

















