KryoNet序列化竞态条件:Kryo持续尝试序列化带引用的条目
针对KryoNet对象图序列化竞态的零成本解决方案
核心问题根因
你之前所有方案失效的本质是并发保护的粒度完全错位:
DoNotModify标记在单艘船的发送循环内设置、清除,等于每发完一艘船就解除写入限制,窗口期完全暴露- 之前用的synchronized/自定义锁没有覆盖「从集合遍历开始、到所有船完成压缩、序列化、发送、解压缩」的全流程,只锁了单个方法调用,中间无保护的间隙足够让AddShip线程插入修改
- Kryo默认开启引用追踪机制,只要序列化过程中碰到未做压缩处理的对象引用,就会递归遍历整个对象图,直接把不该发的全量数据塞到网络包里
可直接落地的无新子系统方案
方案1:同锁互斥覆盖全流程,彻底消除窗口期
直接用Fleet实例本身作为锁对象,把所有涉及shipList遍历、写入的关键逻辑统一纳入同一把锁的临界区,直接删掉无用的DoNotModify标记:
public class Fleet { List<Ship> shipList; public void SendToClient() { // 锁覆盖从拿到集合遍历权到全流程结束的所有步骤,无间隙 synchronized (this) { for (Ship ship : shipList) { ship.compress(); for (int i = 0; i < MultiplayerController.clients.size(); i++) { clients.get(i).connection.sendTCP(ship); } ship.decompress(); } } } public void AddShip(Ship ship) { // 写入操作和发送逻辑抢同一把锁,发送窗口期完全无法插入写操作 synchronized (this) { // 原有耗时处理逻辑 shipList.add(ship); } } }
这个方案完全复用Java内置锁,不需要额外开发组件,之前用synchronized不生效的唯一原因,就是之前没有把全流程的所有步骤都放到同一个锁块里——比如之前只锁了compress方法、又单独锁了sendTCP方法,两个锁块中间的空隙就是竞态发生的位置。
方案2:集合快照缩窄锁范围,避免长锁卡顿
如果你的shipList规模极大,全流程持锁会阻塞游戏主线程的话,只需要把锁的范围缩到极致:
- 只在拷贝集合副本的瞬间加锁,把当前shipList的内容复制为本地临时数组,立刻释放锁
- 后续对临时副本执行compress、发送、decompress的全流程完全不需要加锁——因为副本和原集合完全独立,其他线程往原集合新增多少未压缩的Ship对象,都不会被Kryo序列化到网络包里
这种方式的锁持有时间只有微秒级,完全不会影响游戏逻辑的运行流畅度。
方案3:Kryo配置兜底,从根上阻断对象图递归
就算有小概率的竞态遗漏,也可以通过Kryo的基础配置彻底杜绝异常:
- 关闭Kryo默认开启的引用追踪:调用
kryo.setReferences(false),从根源上取消Kryo顺着对象引用递归遍历整个对象图的行为,让Kryo只序列化当前要发送的对象本身的字段 - 给不允许被递归序列化的全局对象(比如行星、军队实例)配置自定义序列化器:碰到这类对象时,只序列化对应的唯一ID,就算意外被引用到,也只会传递ID值,不会把整个关联对象图打包发送,更不会触发KryoNetException。
内容的提问来源于stack exchange,提问作者ProgrammerMan
相关产品推荐
相关产品推荐

