迭代器的并发破坏:分析多个迭代器同时修改同一变量集合导致的异常
ConcurrentModificationException异常的直接原因是集合在迭代过程中发生了结构性修改,导致集合的modCount与迭代器记录的expectedModCount不一致,从而触发fail-fast机制。问题的核心并非迭代器数量,而是遍历时对集合进行了add或remove等改变结构的操作。要安全地在遍历中修改集合,应使用迭代器自身的rem
ConcurrentModificationException 的根本原因是遍历中发生结构性修改,而非迭代器数量;每个迭代器持有独立 expectedModCount,仅当 modCount 不匹配时触发 fail-fast 机制。

在Ja va开发中,ConcurrentModificationException 这个异常恐怕不少人都遇到过。很多人第一反应是:“是不是因为我开了多个迭代器(Iterator)同时操作同一个集合?” 其实,这个直觉只对了一半。真正引发问题的,往往不是迭代器的数量,而是那个在遍历过程中“偷偷”修改了集合结构的动作——无论这个动作来自另一个迭代器、集合自身的方法,还是同一线程里的其他代码。
为什么“多个迭代器”不是问题根源?
要理解这一点,得先看看迭代器的工作原理。每个迭代器在创建的那一刻,都会悄悄记下集合当前的“修改版本号”——也就是 modCount 的值,并将其存为自己的 expectedModCount。只要后续没有任何代码去改动集合的结构(比如增删元素),那么无论创建多少个迭代器,它们各自安好,完成遍历,都不会出问题。
举个例子:线程A创建了iterator1并顺利遍历完整个集合,线程B创建了iterator2也完成了遍历,这个过程是安全的。但如果在线程A的iterator1遍历到一半时,线程B突然调用了 list.add() 插入了一个新元素,那么集合的 modCount 就变了。等到iterator1再次调用 next() 方法时,它一对比发现手里的 expectedModCount 和集合当前的 modCount 对不上,便会立刻抛出 ConcurrentModificationException。
真正危险的组合:遍历 + 结构修改
说到底,这个异常是Ja va集合“快速失败(fail-fast)”机制在起作用。它不关心到底是谁修改了集合,它只检查一个事实:“当前集合实际的修改次数,是否还和我开始遍历时记录的那个版本号一致?” 一旦不一致,就立刻报错,目的是尽早暴露潜在的并发问题,避免数据状态进入不可预测的混乱。
哪些是典型的高危场景呢?
- 使用
for-each循环(其底层也是迭代器)遍历一个ArrayList,却在循环体内直接调用list.remove(x)来删除元素。 - 一个线程正用
Iterator遍历集合,另一个线程却调用了list.add()来插入新元素。 - 两个
ListIterator同时操作同一个ArrayList,其中一个调用了add()或remove()。 - 在主线程的遍历过程中,某个异步回调或定时任务“悄无声息”地修改了这个集合。
哪些修改会触发异常?
这里有个关键概念:只有**结构性修改(structural modification)** 才会改变那个关键的 modCount 值。哪些操作算结构性修改呢?
- 改变集合大小的操作,比如
add()、remove()、clear(),以及retainAll()、removeAll()这类批量操作。 - 而像
set()方法,它只是替换某个位置的元素,不改变集合大小,因此不算结构性修改,不会触发异常。 - 至于纯粹的只读操作,如
get()、contains()、size(),那就更加安全了。
安全修改的正确方式
如果业务逻辑确实要求在遍历过程中增删元素,那该怎么办?硬来肯定不行,得遵循“规矩”。
- 删除元素:务必使用迭代器自身的
iterator.remove()方法。注意,它只能删除刚刚通过next()返回的那个元素,调用前必须先调用next()。 - 添加元素(仅适用于
ListIterator):使用listIterator.add()方法。这个方法很“聪明”,它在添加元素的同时,会同步更新迭代器内部的expectedModCount,从而避免后续操作抛出异常。 - 批量删除:在Ja va 8及以上版本,推荐使用
Collection.removeIf(Predicate)方法。这是一个原子操作,其内部实现已经巧妙地规避了并发修改检查,既安全又简洁。 - 多线程场景:如果集合真的需要在多线程环境下被频繁读写,那么换用线程安全的集合类才是根本解决方案。例如,读多写少的场景可以考虑
CopyOnWriteArrayList,而需要高性能并发访问的键值对存储则首选ConcurrentHashMap。
理解这些规则,本质上是在理解Ja va集合框架为平衡便利性与安全性所做的设计。下次再遇到这个异常时,不妨先冷静下来,检查一下到底是哪段代码在遍历过程中“越了界”。


































