Java集合迭代失效是fail-fast机制主动暴露的逻辑风险,源于modCount与expectedModCount不一致,单线程边遍历边remove也会触发;应依场景选CopyOnWriteArrayList、ConcurrentHashMap或分片复制等方案,规避synchronized包裹循环等伪解。

Java集合迭代失效(即ConcurrentModificationException)不是偶然报错,而是fail-fast机制主动暴露的逻辑风险。应对复杂并发场景的技术考核,关键不在“捕获异常”,而在于理解机制根源、区分读写模式、选对工具链,并规避常见认知陷阱。
认清本质:失效不是bug,是设计契约
迭代器失效源于集合内部modCount与迭代器持有的expectedModCount不一致。这个检查发生在每次next()或hasNext()调用前——它不区分修改来自本线程还是其他线程,也不区分是添加/删除还是清空操作。因此:
- 单线程中边遍历边调
list.remove()也会触发异常 - 即使用了
Collections.synchronizedList(),迭代过程仍会失效(因为同步只保方法原子性,不保跨调用状态一致性) -
Vector的迭代器也不是Fail-Safe,只是加了synchronized,多线程下仍可能看到脏读或漏遍历
按场景选方案:没有银弹,只有适配
技术考核常给一个高并发订单处理系统,要求“实时遍历活跃订单并清理过期项”。此时需拆解需求:
-
读多写少(如监控大盘):优先用
CopyOnWriteArrayList。迭代器基于快照,写操作复制数组,读完全无锁。注意内存开销和写延迟,不适合高频更新场景 -
读写均衡(如实时消息队列):用
ConcurrentHashMap替代HashMap,配合keySet().iterator()或forEach()。它的迭代器提供弱一致性视图,不抛CME,且支持并发读写 -
大集合分片处理(如批量报表生成):预切块(如按ID取模分10段),每段用
new ArrayList(original.subList(i, j))复制后交由线程独立遍历。避免subList返回的视图被原集合修改影响 -
纯计算型遍历(如风控规则匹配):改用
list.parallelStream().filter(...).map(...).collect()。底层ForkJoinPool自动分片,绕过传统迭代器游标机制
避坑要点:考官最常设的“伪解”陷阱
技术面试中,以下做法看似合理,实则暴露基础不牢:
立即学习“Java免费学习笔记(深入)”;
- 用
synchronized(collection)包裹整个for-each循环:虽能防CME,但把并发遍历退化为串行,违背高并发设计初衷;若嵌套锁还易死锁 - 在for-each里调
collection.remove():无论单线程还是多线程都必报CME,因for-each就是语法糖,等价于iterator.next()+ 检查 - 认为
ConcurrentHashMap的entrySet().iterator()绝对线程安全:它不保证迭代期间看到所有最新变更(弱一致性),但不会抛CME——这点必须说清,否则算概念混淆 - 用
removeIf()代替迭代器删除:它在单线程下安全简洁,但在多线程中若集合本身非线程安全(如普通ArrayList),仍会触发CME
加分项:体现工程权衡意识
真实系统中,还要考虑:
- 如果业务允许最终一致性,可引入读写分离:写入走
ConcurrentLinkedQueue,读取定期快照到CopyOnWriteArrayList - 对超大集合(千万级),避免全量迭代,改用索引+分页查询(如数据库游标思想),或引入
LongAdder做轻量计数替代遍历统计 - 日志中记录CME不应只打堆栈,要补上下文:当前线程名、集合大小、最近一次修改操作类型,便于定位是逻辑误用还是真并发冲突


















