ConcurrentModificationException 的根本原因在于遍历过程中发生了结构性修改,而非迭代器数量;每个迭代器都维护独立的 expectedModCount 副本,仅当检测到 modCount 不匹配时才会触发 fail-fast 保护机制。

在 Java 开发实践中,ConcurrentModificationException 是开发者经常遇到的运行时异常。许多开发者误以为问题的根源在于同时使用了多个迭代器(Iterator)操作同一集合。实际上,异常产生的核心并非迭代器数量,而是任何在迭代过程中对集合结构进行的“隐蔽”修改——无论修改来自另一个迭代器、集合自身的非迭代器方法,还是同一线程内的其他代码段。
为何“多个迭代器”并非异常的根本原因?
要透彻理解这一点,需要深入迭代器的工作机制。每个迭代器在实例化时,都会捕获并存储集合当前的“结构修改计数器”——即 modCount 字段的值,并将其保存为自身的 expectedModCount。只要集合的结构(如元素数量)在后续过程中保持不变,那么无论创建多少个迭代器进行遍历,都不会引发任何异常。
举例说明:线程A创建 iterator1 并完整遍历集合,线程B创建 iterator2 也独立完成遍历,此过程完全安全。然而,若在线程A的 iterator1 遍历中途,线程B通过 list.add() 方法插入了一个新元素,集合的 modCount 随即递增。当 iterator1 再次调用 next() 或 remove() 方法时,它会校验内部的 expectedModCount 与集合当前的 modCount 是否一致。一旦发现不匹配,便会立即抛出 ConcurrentModificationException 以终止操作。
真正的风险组合:迭代遍历与结构性修改并发
本质上,该异常是 Java 集合框架“快速失败(fail-fast)”安全机制的体现。该机制不关注修改者的身份,只验证一个核心条件:“集合自迭代开始以来,其结构是否被意外更改?” 一旦检测到不一致,便快速抛出异常,旨在防止程序进入数据状态不可控的境地。
哪些是典型的易发场景?
- 使用增强型
for-each循环(底层依赖迭代器)遍历ArrayList,却在循环体内直接调用list.remove(element)删除元素。 - 一个线程正在使用
Iterator遍历集合,另一线程并发调用list.add()或list.remove()修改集合结构。 - 两个
ListIterator同时操作同一ArrayList,其中一个调用了add()或remove()方法。 - 在主线程迭代过程中,某个异步任务(如定时器或回调函数)意外修改了被遍历的集合。
哪些操作会触发异常?
关键在于区分**结构性修改(Structural Modification)** 与非结构性修改。只有结构性修改会递增 modCount 值,从而可能引发异常。
- 会触发异常的操作:改变集合大小的操作,如
add()、remove()、clear(),以及retainAll()、removeAll()等批量操作方法。 - 不会触发异常的操作:
set(index, element)方法仅替换指定位置的元素,不改变集合大小;纯粹的查询操作如get()、contains()、size()则完全安全。
如何在遍历中安全地修改集合?
若业务逻辑确实需要在迭代过程中增删元素,必须采用正确的方法来规避 ConcurrentModificationException。
- 安全删除元素:务必使用迭代器自身的
Iterator.remove()方法。注意,该方法只能删除最近一次next()调用返回的元素,且调用前必须执行过next()。 - 安全添加元素(仅限
ListIterator):使用ListIterator.add(element)方法。此方法在添加元素的同时,会同步更新迭代器内部的expectedModCount,确保后续操作正常。 - 批量条件删除:在 Java 8 及以上版本,推荐使用
Collection.removeIf(Predicate filter)方法。其内部实现已优化,能安全高效地完成过滤删除。 - 多线程并发场景:若集合需在多个线程间高频读写,应直接选用线程安全的集合实现。例如,读多写少的场景可使用
CopyOnWriteArrayList;高并发键值存储则可选用ConcurrentHashMap。
深入理解这些规则,有助于掌握 Java 集合框架在便捷性与数据一致性之间的设计权衡。再次遭遇此异常时,建议首先排查代码中是否存在“遍历过程中进行非法结构修改”的代码段,从而快速定位问题根源。
