objectoutputstream多次写入同一对象实例时读出旧值,因其内部引用缓存表复用句柄跳过字段重写;writeobject启用引用共享,writeunshared强制全量序列化;安全做法是每次writeobject后调用reset()清空缓存。

Java 中 ObjectOutputStream 在多次调用 writeObject() 写入**同一个对象实例**时,不会重复序列化其完整字段,而是写入一个轻量级的“引用句柄”(reference handle)。这是它的默认引用替换机制,目的是节省空间、提升性能,但容易引发字段值未更新的隐蔽问题。
为什么会出现“写入相同对象却读出旧值”?
ObjectOutputStream 内部维护一个对象引用缓存表(handle table),记录已序列化过的每个对象及其分配的整数句柄。当第二次 writeObject(obj) 传入与之前完全相同的对象引用(== 判定为 true)时:
- 流查表发现该对象已有句柄,直接写入该句柄(如 TC_REFERENCE + handle ID)
- 不重新遍历字段,不写入 currentLobbyId 等已修改的值
- ObjectInputStream 反序列化时,复用之前创建的对象实例,跳过字段赋值流程
- 结果:客户端看到的仍是第一次序列化时的字段快照
writeObject 与 writeUnshared 的关键区别
writeObject(obj):启用引用共享。同一对象多次写入 → 多次句柄,反序列化后指向同一实例。
writeUnshared(obj):禁用引用共享。每次写入都强制深拷贝式序列化,生成独立对象实例,即使内容相同也写全字段。
注意:writeUnshared 不等于“克隆对象”,它只是绕过缓存、不写句柄;对象本身仍需可序列化,且反序列化后是新实例(!= 原对象)。
如何安全地反复发送状态变更的对象?
最常用且明确可控的方式是手动重置流缓存:
- 每次
writeObject(response)后立即调用out.reset() -
reset()清空内部句柄表和已写类描述,后续对象全部重新完整序列化 - 适合周期性推送(如心跳、状态快照)、对象字段频繁变更的场景
- 开销轻微,远小于引入 JSON/Protobuf 的改造成本
其他可行但需权衡的方案
若 reset 频率过高或需更精细控制:
- 重写
writeReplace(),在序列化前返回一个新构造的不可变副本 - 使用
putFields()+writeFields()显式控制每次写入的字段集合 - 彻底切换协议:用 Jackson/Protobuf 替代原生序列化,天然无引用缓存副作用
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











