
反序列化会绕过构造方法和所有初始化逻辑,直接从字节流“拼出”一个新对象——哪怕你用了双重检查锁、静态内部类或枚举以外的任何常规写法,只要实现了 Serializable,就可能产生第二个实例。关键防护手段就是 readResolve():它在反序列化完成后的最后一刻介入,用原始单例实例替换掉刚创建的新对象。
readResolve() 的基本写法和要求
必须严格满足以下条件,JVM 才会识别并调用它:
- 方法签名固定为 private Object readResolve() throws ObjectStreamException
- 返回值必须是你要保护的那个唯一实例(例如
return INSTANCE;或return instance;) - 修饰符必须是 private;public / protected / default 均无效
- 所在类必须实现 Serializable 接口,否则不会触发该机制
为什么光有 readResolve() 还不够
它只防反序列化,不防反射攻击。如果攻击者通过反射调用私有无参构造器,readResolve() 完全不生效。所以需组合防御:
- 构造器中加校验:首次初始化后,再次调用就抛异常(如
if (INSTANCE != null) throw new RuntimeException(...)) - 所有非 transient 的对象引用字段,建议声明为 transient,避免在 readResolve() 执行前被篡改或窃取
- 确保 INSTANCE 是 static final 或线程安全初始化完成的,否则 readResolve() 可能返回 null
比 readResolve() 更彻底的方案:直接用枚举
如果单例不需要继承其他类、也不需要实现复杂接口,推荐一步到位使用枚举:
- Java 规范强制保证枚举类型的序列化/反序列化天然单例
- JVM 内部硬编码了
readResolve()行为,无需手动编写 - 同时免疫反射、反序列化、多类加载器隔离等常见破坏路径
- 写法极简:
public enum Singleton { INSTANCE; }
验证是否生效的小技巧
写个简单测试:序列化单例对象到文件,再反序列化出来,用 == 比较两个引用。
- 没加 readResolve() → 输出
false - 加了且写法正确 → 输出
true - 用了枚举 → 同样输出
true,且无需额外代码

















