fail-fast机制通过modCount与expectedModCount校验实现,用于检测遍历中结构被意外修改;单线程for-each中直接list.remove()会触发ConcurrentModificationException;安全做法是用Iterator.remove()、removeIf()或批量删除;CopyOnWriteArrayList因迭代器基于不可变快照而不校验modCount,适合读多写少场景。

大厂面试问到集合迭代失效(fail-fast),不是想听你复述“会抛 ConcurrentModificationException”,而是看你能不能把异常背后的源码逻辑、设计意图、真实场景和应对方案串成一条线。答得清楚,说明你真看过 ArrayList 和 Iterator 的源码;答得模糊,哪怕背过定义,也容易被追问两轮就露馅。
核心要讲清:modCount 和 expectedModCount 是什么关系
这是所有 fail-fast 问题的源头。ArrayList 内部有个 modCount 字段,记录结构修改次数(比如 add、remove、clear);而每个 Iterator 实例创建时,会把当时的 modCount 复制一份存为 expectedModCount。每次调用 next() 或 remove() 前,都会校验二者是否相等——不等就立刻 throw new ConcurrentModificationException。
这不是为了防“多线程”,而是防“遍历中被意外修改”。哪怕单线程里,在 for-each 循环里直接 list.remove(),也会触发它。
高频追问怎么答:为什么 remove() 方法不重置 expectedModCount?
因为 Iterator 的 remove() 是安全的删除方式,它内部会同步更新 expectedModCount,所以不会报错。但注意:这个 remove() 必须紧跟在 next() 之后调用,且只能删一次。常见错误是:
立即学习“Java免费学习笔记(深入)”;
- 在 for-each 中写 list.remove(x) —— 错,触发 fail-fast
- 用普通 for 循环,从前往后删匹配元素 —— 错,会漏删(下标偏移)
- 用 iterator.remove() 但没先调 next() —— 报 IllegalStateException
正确做法是:用迭代器自己的 remove(),或改用 removeIf()(JDK 8+)、收集待删元素再批量删。
线程安全场景不能只说“用 CopyOnWriteArrayList”
面试官如果接着问“那 CopyOnWriteArrayList 为什么不会 fail-fast?”,光答“它不检查 modCount”不够。要指出关键差异:
- 它的 iterator() 返回的是快照副本(new COWIterator),底层数组是不可变的
- 所有写操作(add/remove)都新建数组并复制,原 iterator 继续读旧数组,自然无冲突
- 代价是内存开销大、写操作慢、读操作不感知实时变更——适合读多写极少的场景,比如监听器列表
顺便对比:Collections.synchronizedList() 只是给方法加 synchronized,迭代时仍需手动同步整个遍历块,否则依然可能出问题。
结合业务场景讲排查和选型
面试最后常落到“你怎么选”。别只列类名,要带判断依据:
- 单线程遍历中要边查边删 → 用 Iterator.remove() 或 ListIterator
- 需要并发读写,且写操作少、读操作极频繁 → CopyOnWriteArrayList
- 需要强一致性,读写都要锁 → Collections.synchronizedList + 外部同步块
- 高并发、复杂操作 → 改用 ConcurrentHashMap 或 BlockingQueue 等更合适的结构
一句话收尾:fail-fast 不是 bug,是 JDK 主动暴露问题的设计哲学——宁可停机,也不让数据状态变得不可知。


















