反序列化会绕过构造器直接重建对象,破坏单例;需用readResolve()修复或改用枚举单例。Java反序列化不调用构造器、初始化块,仅按字节流填充字段,导致单例被绕过;readResolve()可拦截并替换为原实例,但仅防反序列化;枚举单例由JVM强制保障唯一性,天然防反射与反序列化攻击。

Java 反序列化会绕过构造器,直接在堆上重建对象——哪怕单例类加了“禁止重复实例”的防护,反序列化也能悄悄生成第二个实例。这不是逻辑错误,而是 JVM 序列化机制的设计特性。
为什么反序列化能破坏单例
Java 默认反序列化不调用任何构造方法(包括私有构造器),也不执行静态初始化块或实例初始化代码。它仅根据字节流中的字段值,直接分配内存并填充字段。只要单例类实现了 Serializable 接口,且没做额外处理,反序列化就会产生一个全新对象,与原有单例实例完全独立。
- 即使构造器里写了
if (INSTANCE != null) throw new RuntimeException(),也完全无效 - 该漏洞在 Redis 缓存、RPC 调用、消息队列等场景中极易触发——比如把单例对象序列化后存入 Redis,再读取反序列化,就得到“假单例”
- 此时
==判断失败,hashCode()不同,状态不同步,分布式环境下可能引发数据错乱
readResolve() 是关键补救手段
这是 Java 序列化规范提供的钩子方法:只要类中定义了 private Object readResolve(),反序列化引擎会在对象构建完成后、返回给调用方之前,自动调用它,并用其返回值替换刚创建的对象。
- 必须声明为
private,返回类型为Object,方法名严格为readResolve - 典型写法:
private Object readResolve() { return INSTANCE; } - 它只对反序列化路径生效,不能防反射攻击,所以不能单独依赖
枚举单例是终极解法
JVM 对枚举类型做了硬性保障:既不允许反射调用枚举构造器(Constructor.newInstance() 会抛 java.lang.EnumException),又在序列化/反序列化时强制复用已有枚举常量(readResolve 由 JVM 内部隐式实现)。
立即学习“Java免费学习笔记(深入)”;
- 写法极简:
public enum Singleton { INSTANCE; } - 天然线程安全、懒加载(首次访问才初始化)、防反射、防反序列化克隆
- 无需手写
readResolve,也不用在构造器里加状态校验
非枚举方案必须双管齐下
如果因历史原因无法改用枚举,必须同时堵住反射和反序列化两个缺口:
- 构造器内加实例检查:用
static volatile boolean initialized = false控制,首次设为 true,后续抛异常 - 必须实现
readResolve()并返回唯一实例 - 注意:静态内部类单例 +
readResolve仍不安全——静态内部类的构造器可被反射调用,readResolve对反射无效


















