
1. 背景与核心概念在软件开发中我们常常会遇到一些看似简单实则暗藏玄机的“小问题”。这些问题可能不涉及复杂的算法或高深的架构但若处理不当却足以让开发者耗费大量时间在调试和排查上严重影响开发效率和心情。今天我们要“轻松拿捏”的正是这类在Java集合操作中高频出现的一个经典异常ConcurrentModificationException。它是什么ConcurrentModificationException是 Java 运行时异常RuntimeException的一个子类。顾名思义它通常在“并发修改”时被抛出。但这里有一个常见的误解它并非专为多线程场景设计。在单线程环境下如果你在使用迭代器Iterator遍历一个集合如ArrayList,HashMap的同时又通过集合自身的方法非迭代器的方法去修改集合的结构增、删元素就会立刻触发此异常。它解决什么问题这个异常是 Java 集合框架Java Collections Framework设计中的一种“快速失败”Fail-Fast机制。它的核心目的是为了保证迭代器在遍历过程中能感知到集合结构被意外修改从而避免产生不可预期的行为例如数据错乱、无限循环或程序崩溃。它像一个尽职的哨兵在你可能犯错的时候及时发出警报。为什么需要掌握无论是刚入门的新手还是经验丰富的开发者在编写涉及集合遍历和修改的代码时几乎都“邂逅”过这个异常。理解其产生原理不仅能快速解决眼前的问题更能深入理解 Java 集合迭代器的工作机制和设计思想从而写出更健壮、更安全的代码。本文将带你从异常现象出发深入原理彻底搞懂如何在不同场景下规避和正确处理它真正做到“轻松拿捏”。2. 环境准备与版本说明本文的代码示例和讲解基于以下通用环境核心原理适用于大多数 Java 版本。JDK 版本 8 或以上本文示例使用 JDK 8 语法高版本完全兼容。ConcurrentModificationException机制在集合框架早期版本就已存在。集成开发环境IDE IntelliJ IDEA, Eclipse 或 VS Code 均可。使用 IDE 可以更方便地运行和调试示例。构建工具 无特定要求示例为纯 Java 代码。项目结构 一个简单的 Java 项目即可包含src目录和主类。版本兼容性说明 本文重点在于阐述普适性原理和解决方案。虽然 Java 后续版本如 JDK 1.7 的CopyOnWriteArrayList JDK 1.8 的Collection.removeIf提供了更多内置的解决方案但ArrayList和HashMap等核心类的“快速失败”迭代器行为基本保持一致。理解基础原理是运用高级 API 的前提。3. 核心原理与异常触发场景拆解要“拿捏”这个异常必须搞清楚两件事什么是“并发修改”以及迭代器如何检测到这种修改。3.1 “并发修改”的真实含义这里的“并发”并非特指多线程并发。在单线程中它指的是同一时间片段内发生了两种冲突的操作操作A 使用迭代器遍历集合元素。操作B 使用集合对象本身的方法非该迭代器的方法对集合进行结构性修改。结构性修改是指任何会改变集合大小size或内部数组结构如HashMap的 rehash的操作例如add(E e)remove(Object o)clear()非结构性修改如使用set(int index, E element)更新ArrayList中已存在索引的元素值通常不会抛出此异常但并非绝对安全取决于具体集合实现。3.2 迭代器的“快速失败”机制以最常用的ArrayList为例其内部维护了一个modCount修改计数器变量。任何会改变集合结构的操作如add,remove都会使modCount加一。当你通过list.iterator()获取一个迭代器时迭代器会“记住”当前集合的modCount值我们称之为expectedModCount。在迭代器执行next(),remove(),hasNext()等关键方法时它会首先检查当前的modCount是否等于expectedModCount。如果不相等则说明在迭代器创建之后有其他“东西”可能是集合自身的方法也可能是另一个迭代器修改了集合结构。此时迭代器会立即抛出ConcurrentModificationException而不是冒着风险继续执行这就是“快速失败”。3.3 典型错误代码示例下面我们通过几个经典错误示例直观感受异常是如何被触发的。场景一遍历时删除元素错误示范这是最常见的触发场景。import java.util.ArrayList; import java.util.List; public class CMEExample1 { public static void main(String[] args) { ListString list new ArrayList(); list.add(Java); list.add(Python); list.add(Golang); // 错误做法使用 for-each 循环底层也是迭代器遍历时调用 list.remove() for (String language : list) { if (Python.equals(language)) { list.remove(language); // 这里会抛出 ConcurrentModificationException! } } System.out.println(list); } }运行结果与解释 程序在尝试删除Python后下一次循环调用迭代器的hasNext()或next()方法时会检测到modCount已变而expectedModCount未变随即抛出异常。for-each循环本质上是迭代器遍历的语法糖。场景二使用迭代器时用集合方法删除错误示范import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class CMEExample2 { public static void main(String[] args) { ListInteger numbers new ArrayList(); for (int i 0; i 5; i) { numbers.add(i); } IteratorInteger iterator numbers.iterator(); while (iterator.hasNext()) { Integer num iterator.next(); if (num % 2 0) { numbers.remove(num); // 错误使用集合的remove方法而非迭代器的 // 执行此行后下一轮 iterator.next() 会抛出异常 } } } }场景三多线程下的不安全操作错误示范即使每个线程单独看代码正确多线程共享同一集合且未同步时也会因modCount的意外变更触发异常。import java.util.ArrayList; import java.util.List; public class CMEExample3 { private static ListString sharedList new ArrayList(); public static void main(String[] args) throws InterruptedException { sharedList.add(A); sharedList.add(B); sharedList.add(C); // 线程1 迭代遍历 Thread thread1 new Thread(() - { for (String s : sharedList) { // 使用迭代器 try { Thread.sleep(100); // 模拟处理耗时 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(s); } }); // 线程2 修改集合 Thread thread2 new Thread(() - { try { Thread.sleep(50); // 确保线程1先开始迭代 } catch (InterruptedException e) { e.printStackTrace(); } sharedList.add(D); // 在线程1迭代过程中修改结构 }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); } }运行此程序有很大概率在线程1的迭代过程中因为线程2的add操作导致modCount变化从而抛出ConcurrentModificationException。4. 完整实战如何正确地在遍历时修改集合明白了“病因”接下来就是“对症下药”。我们针对不同场景提供安全且高效的解决方案。4.1 方案一使用迭代器自身的remove()方法单线程首选这是解决单线程遍历删除问题的标准答案。迭代器提供的remove()方法会在删除元素后同步更新它内部维护的expectedModCount使其与集合的modCount保持一致从而避免异常。import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class SafeRemoveByIterator { public static void main(String[] args) { ListString list new ArrayList(); list.add(Apple); list.add(Banana); list.add(Cherry); list.add(Date); System.out.println(删除前: list); IteratorString iterator list.iterator(); while (iterator.hasNext()) { String fruit iterator.next(); if (fruit.startsWith(B)) { iterator.remove(); // 关键使用迭代器的remove方法 } } System.out.println(删除后: list); // 输出: [Apple, Cherry, Date] } }4.2 方案二使用CopyOnWriteArrayList多线程场景CopyOnWriteArrayList是java.util.concurrent包下的线程安全集合。它的策略是“写时复制”任何修改操作add, set, remove都会在底层创建一个原数组的新副本修改在新副本上进行而迭代器始终遍历不变的旧数组快照。因此遍历和修改可以完全并发进行不会抛出ConcurrentModificationException。适用场景读多写少且需要遍历稳定性的多线程环境。缺点写操作尤其是频繁写开销大因为需要复制数组。import java.util.Iterator; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; public class SafeWithCopyOnWriteArrayList { public static void main(String[] args) { ListInteger numbers new CopyOnWriteArrayList(); for (int i 0; i 5; i) { numbers.add(i); } // 线程1 迭代遍历 Thread thread1 new Thread(() - { IteratorInteger it numbers.iterator(); while (it.hasNext()) { System.out.println(Thread1 reading: it.next()); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } }); // 线程2 修改集合 Thread thread2 new Thread(() - { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } numbers.add(99); // 安全添加不会影响线程1的迭代 System.out.println(Thread2 added 99); }); thread1.start(); thread2.start(); try { thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Final list: numbers); } }4.3 方案三使用 Java 8 的Collection.removeIf()方法单线程简洁方案Java 8 在Collection接口中引入了removeIf(Predicate filter)方法。它内部会优化删除过程对于ArrayList它最终会通过迭代器或系统拷贝的方式安全删除是单线程下条件删除的最佳实践代码非常简洁。import java.util.ArrayList; import java.util.List; public class SafeRemoveByRemoveIf { public static void main(String[] args) { ListString list new ArrayList(); list.add(Cat); list.add(Dog); list.add(Elephant); list.add(Fox); System.out.println(删除前: list); // 使用 removeIf 删除长度大于 3 的字符串 list.removeIf(animal - animal.length() 3); System.out.println(删除后: list); // 输出: [Cat, Dog, Fox] } }4.4 方案四遍历时记录遍历后删除适用于复杂条件如果删除逻辑非常复杂或者需要根据遍历结果进行多次、多种修改可以先记录下需要删除的元素遍历结束后再统一处理。import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class SafeRemoveByCollectFirst { public static void main(String[] args) { ListPerson personList new ArrayList(); personList.add(new Person(Alice, 25)); personList.add(new Person(Bob, 17)); personList.add(new Person(Charlie, 30)); personList.add(new Person(David, 16)); System.out.println(删除前: personList); // 方案4.1: 记录待删除对象的引用 ListPerson toBeRemoved new ArrayList(); for (Person p : personList) { if (p.age 18) { // 复杂条件判断 toBeRemoved.add(p); } } personList.removeAll(toBeRemoved); // 方案4.2: 记录待删除元素的索引适用于ArrayList // ListInteger indexesToRemove new ArrayList(); // for (int i 0; i personList.size(); i) { // if (personList.get(i).age 18) { // indexesToRemove.add(i); // } // } // // 从后往前删避免索引错位 // for (int i indexesToRemove.size() - 1; i 0; i--) { // personList.remove(indexesToRemove.get(i).intValue()); // } System.out.println(删除后: personList); // 输出: [Alice(25), Charlie(30)] } static class Person { String name; int age; Person(String name, int age) { this.name name; this.age age; } Override public String toString() { return name ( age ); } } }4.5 方案五使用传统 for 循环与索引仅适用于ArrayList需谨慎对于ArrayList你可以使用传统的for循环和索引并从后往前删除。这是因为从后往前删除不会影响前面待遍历元素的索引。import java.util.ArrayList; import java.util.List; public class SafeRemoveByIndex { public static void main(String[] args) { ListString list new ArrayList(); list.add(One); list.add(Two); list.add(Three); list.add(Two); list.add(Four); System.out.println(删除前: list); // 从后往前遍历删除 for (int i list.size() - 1; i 0; i--) { if (Two.equals(list.get(i))) { list.remove(i); // 使用List的remove(int index)方法 } } System.out.println(删除后: list); // 输出: [One, Three, Four] } }注意此方法不适用于LinkedList因为LinkedList的get(int index)和remove(int index)是 O(n) 操作性能极差且索引计算在删除中间元素后依然会错乱。5. 常见问题与排查思路即使知道了正确方法在实际编码中仍可能遇到一些变种问题。下表整理了常见场景和排查方向。问题现象常见原因解决思路在for-each循环中调用list.add/remove抛异常误用集合方法修改结构。改用迭代器iterator.remove()或removeIf。使用iterator遍历但抛异常可能混用了集合的修改方法或存在多线程并发修改。1. 检查循环内是否误用了list.add/remove。2. 检查是否为多线程环境考虑使用CopyOnWriteArrayList或加锁。使用Collections.synchronizedList包装后仍抛异常同步包装只保证单个方法调用原子性不保证迭代安全。在迭代代码块上手动加锁 (synchronized(list) { ... })。更新HashMap的Value值不抛异常但更新Key或增删抛异常修改Value不属于结构性修改modCount不变。修改Key或增删属于结构性修改。遍历时不要增删条目或修改作为Key的对象影响hashCode。需要增删时记录到临时集合遍历后处理。使用Stream.forEach时修改集合抛异常Stream的终端操作内部也可能使用迭代器或拆分器。不要在对源集合的Stream进行终端操作的过程中修改它。先收集结果再修改源集合。在迭代List的子列表subList时修改原List抛异常subList的迭代器会检查原List的modCount。避免在迭代子列表视图时修改其背后的原列表。通用排查步骤定位异常栈找到抛出ConcurrentModificationException的代码行通常位于next(),checkForComodification()等方法中。识别遍历方式确认是for-each、显式Iterator、Stream还是其他方式。搜索修改点在遍历代码块内外搜索所有对目标集合进行结构性修改add/remove的语句。判断执行路径确认修改语句是否有可能在遍历开始后、结束前被执行。检查线程安全如果涉及多线程考虑线程安全问题。选择解决方案根据场景单线程/多线程删除/增加性能要求选择上文对应的正确方案。6. 最佳实践与工程建议掌握了解决方案后遵循一些最佳实践能从根本上减少此类错误的发生。优先使用for-each循环进行只读遍历当你的目的仅仅是读取集合元素时for-each循环是最清晰、最安全的选择。它从语法上提醒你这是只读操作。需要删除时首选removeIf()在单线程环境下需要根据条件删除元素Collection.removeIf()是代码最简洁、意图最明确的方式应作为首选。显式使用Iterator以获得最大控制权当遍历逻辑复杂可能需要在循环中根据多种条件进行remove或者需要调用iterator.remove()以外的迭代器方法时应显式使用Iterator对象。多线程环境下的集合选择读多写少 使用CopyOnWriteArrayList或CopyOnWriteArraySet。写多或需要高性能 使用ConcurrentHashMap代替HashMap使用java.util.concurrent包下的阻塞队列如LinkedBlockingQueue代替List。注意即使使用ConcurrentHashMap其迭代器也是弱一致性的不保证能反映创建后的所有修改但不会抛ConcurrentModificationException。需要对迭代过程加锁 使用Collections.synchronizedCollection()包装后在迭代代码块上手动同步。避免在遍历过程中进行非预期的结构修改在编写遍历代码时要有意识地审视循环体内部和外部如通过回调、监听器是否有可能修改当前正在遍历的集合。对于作为参数传入的集合如果不确定其是否会被修改可以考虑在遍历前进行防御性拷贝new ArrayList(originalList)。代码审查时重点关注在团队代码审查中将“在for-each循环或Iterator遍历中对集合进行add/remove”作为一个常见的审查点能有效在早期发现潜在问题。理解不同集合的实现差异ArrayList和LinkedList的迭代器行为在ConcurrentModificationException上是一致的但它们的性能特性和适用场景不同。HashMap的迭代器同样具有快速失败特性。了解这些底层实现能帮助你做出更合适的技术选型。7. 总结ConcurrentModificationException是 Java 集合框架给开发者的一个“善意提醒”它迫使我们去思考遍历与修改的并发性问题。通过本文的梳理我们可以总结出清晰的应对策略核心根源 迭代器的“快速失败”机制通过对比modCount和expectedModCount来检测非法的并发修改。单线程场景 需要删除元素时放弃集合的remove方法改用迭代器的remove()或更优雅的removeIf()。多线程场景 根据读写比例选择CopyOnWriteArrayList、ConcurrentHashMap或手动同步的线程安全集合。通用原则 遍历与修改分离。如果逻辑复杂采用先收集、后处理的策略总是安全的。真正“轻松拿捏”这个异常意味着你不仅记住了几种解决方法更理解了其背后的设计哲学和契约。下次在代码中看到ConcurrentModificationException时希望你能会心一笑然后熟练地运用最合适的方法将其化解。