线上出现 ConcurrentModificationException,你会怎么定位?换成并发集合就一定没问题吗?
先说结论:ArrayList、HashMap 等普通集合的迭代器通常是 fail-fast:迭代器记录预期修改次数,发现集合被非迭代器路径结构性修改时尽快抛出异常。并发容器通常提供快照或弱一致迭代,不抛该异常但也不承诺强一致快照。
我先给结论,再说明它在项目里解决什么问题。理解结构修改检测、弱一致遍历以及并发修改异常的正确处理方式。ArrayList、HashMap 等普通集合的迭代器通常是 fail-fast:迭代器记录预期修改次数,发现集合被非迭代器路径结构性修改时尽快抛出异常。并发容器通常提供快照或弱一致迭代,不抛该异常但也不承诺强一致快照。 “fail-safe”不是 Java 集合 API 的正式术语,面试中应进一步说明具体容器到底是复制快照还是弱一致读取。
核心机制我会按一次真实执行过程来讲。沿着「创建迭代器并记录版本 → 读取下一个元素 → 比较结构修改计数 → 发现不一致抛异常 → 通过迭代器安全删除」观察输入、状态与输出,这些阶段都可以从日志、指标或源码里验证。 创建迭代器并记录版本 迭代器创建时保存集合 modCount,只有结构性修改通常会递增该计数。 读取下一个元素 next 不只是返回元素,还检查当前位置、边界与预期版本。
实现细节只抓关键入口,不会整段背源码。java.util.ArrayList.Itr#checkForComodification:expectedModCount 与 modCount。 java.util.Collection#removeIf:迭代协议内批量删除。
放到生产使用时,我会关注参数和验证数据。分别在增强 for、Iterator.remove、removeIf 与并发写下执行删除,记录结果和异常;证明异常不是可靠并发检测。 固定输入和基线 先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「modCount 差异」为主基线,记录值应满足「合法迭代始终一致」;同时保存 并发修改异常堆栈、集合修改来源,使后续变化能够回到同一时间轴比较。
最后补充常见误区和使用边界。代码在增强 for 中直接调用 list.remove,修改了集合版本但迭代器不知道。单线程场景改用 removeIf 或 Iterator.remove;并发场景则重新定义快照、锁或并发集合语义,而不是 catch 后重跑。 把 fail-fast 当成线程安全机制:并发修改异常堆栈:单线程删除用 removeIf/Iterator.remove。 方案:更适合的场景:主要收益:代价与边界。 Iterator.remove/removeIf:单线程遍历期间删除:遵守迭代协议、代码清晰:复杂复合修改仍需谨慎。 复制快照后遍历:读视图可稍旧且集合规模可控:遍历不受后续写影响:复制内存与一致性延迟。 并发集合弱一致迭代:并发读写且允许看到部分更新:不抛 fail-fast、无需全局锁:视图不是固定时点快照。