ConcurrentModificationException在单线程中出现并非并发问题,而是遍历集合时直接修改结构违反fail-fast机制;其根本原因是modCount与expectedModCount不一致,正确做法包括使用Iterator.remove()、removeIf()或倒序索引删除。

ConcurrentModificationException 在单线程中出现,根本不是“并发”问题,而是代码逻辑违反了集合的遍历-修改契约。它本质是 Java 集合的 fail-fast(快速失败)机制在报警:你正在一边读、一边偷偷改结构,而集合不允许这样操作。
为什么单线程也会抛这个异常?
ArrayList、HashMap 等非线程安全集合内部维护一个 modCount 计数器,记录结构修改次数(如 add/remove/clear)。当你调用 iterator() 或进入 for-each 循环时,迭代器会把当时的 modCount 值存为 expectedModCount。后续每次调用 next() 或 hasNext(),都会检查两者是否一致——一旦发现不等(比如你在循环里调了 list.remove()),立刻抛出异常。
这不是多线程竞争,而是同一段代码里,**遍历动作和结构修改动作没协调好**。
典型错误写法与对应修复方式
以下都是单线程常见“踩坑”模式,附带正确解法:
立即学习“Java免费学习笔记(深入)”;
-
错:for-each 中直接 remove
for (String s : list) { if (s.equals("xxx")) list.remove(s); }
→ 这会触发 fail-fast,因为 for-each 底层就是 Iterator,但你绕过它去动集合。 -
错:Iterator 遍历时调集合的 remove
Iterator<String> it = list.iterator(); while(it.hasNext()) { String s = it.next(); if (s.equals("xxx")) list.remove(s); }
→ 同样破坏了迭代器预期状态。 -
对:用 Iterator 自己的 remove 方法
Iterator<String> it = list.iterator(); while(it.hasNext()) { String s = it.next(); if (s.equals("xxx")) it.remove(); }
→ 这个remove()是安全的:它会同步更新expectedModCount,并校验lastRet合法性。 -
对:用 Collection.removeIf(Java 8+)
list.removeIf(s -> s.equals("xxx"));
→ 内部封装了安全遍历逻辑,语义清晰,推荐用于简单条件删除。 -
对:倒序 for 循环 + 索引 remove
for (int i = list.size() - 1; i >= 0; i--) { if (list.get(i).equals("xxx")) list.remove(i); }
→ 避开正向遍历时索引偏移问题,且不依赖迭代器,无 fail-fast 检查。
排查技巧:一眼定位问题位置
异常堆栈通常指向 iterator.next() 或 hasNext() 调用处,但真正的问题在它之前的某次 remove()、add() 或 clear()。你可以:
- 搜索当前方法内所有
.remove(、.add(、.clear()调用; - 重点检查它们是否出现在
for-each、while(it.hasNext())或stream().forEach()等遍历结构内部; - 用 IDE 的 “Find Usages” 功能,追踪该集合变量的所有修改点,看是否有隐蔽的修改逻辑(比如被其他方法间接调用)。
额外提醒:别被名字误导
虽然叫 ConcurrentModificationException,但它在单线程下高频出现。不要一看到就怀疑线程安全问题——先确认是不是自己写的循环里动了集合。如果是多线程场景,那才需要考虑 CopyOnWriteArrayList、ConcurrentHashMap 或加锁等方案。


















