DevFix
Java已验证

ConcurrentModificationException 排查:单线程遍历改集合也会中招

@debug_master更新于 3 天前阅读 6 min0

一句话先说结论:这个异常的名字有误导性。它不要求多线程,单线程里一边遍历一边改集合就会抛。根子是 fail-fast 迭代器拿 modCountexpectedModCount 对不上。修法就两条:用 iterator.remove(),或者 Java 8 的 removeIf()

背景

ConcurrentModificationException 是 Java 集合框架里最常见的运行时异常之一。名字里带「并发」,但它跟多线程没有必然关系。

ArrayListHashMapHashSet 这些标准集合都用 fail-fast 迭代器。迭代器一旦发现集合被「结构性修改」,立刻抛异常,而不是等你拿到一个不确定的遍历结果。

「结构性修改」指增删元素、改变容量这类操作,替换已有元素不算。

现象

一个单线程程序,遍历时删掉匹配元素,直接报错。

List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : list) {
    if (s.equals("b")) {
        list.remove(s);
    }
}

报错原文:

Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
	at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)
	at com.example.Main.process(Main.java:15)

HashMap 也一样,遍历 entrySet 时删 key:

java.util.ConcurrentModificationException
	at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1597)

注意堆栈落在 next(),不是你的业务代码那行。很多人第一反应去 process() 找问题,实际根子在更早的遍历写法上。

根因分析

每个集合内部维护一个 modCount 计数器,每次结构性修改就加 1。

迭代器创建时,把当时的 modCount 记作 expectedModCount。之后每次 next() 都校验一次,两个值不一致就抛异常。

ArrayListItr 大致是这么写的:

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

for (String s : list) 本质是隐式拿到迭代器,再循环调 next()。你在循环体里调 list.remove()modCount 变了,迭代器的 expectedModCount 没变,下一次 next() 就炸了。

官方文档还补了一句,容易被忽略:fail-fast 是「尽力而为」,不是硬保证。改了集合有时不抛异常,也可能发生。别把「没抛异常」当成「安全」。

解决方案

首选:removeIf(Java 8+)

一行搞定,任何 Collection 都能用。

list.removeIf(s -> s.equals("b"));

它内部处理好并发校验,天生安全。

手动循环用 Iterator.remove

不用 for-each,显式拿迭代器,删的时候调迭代器自己的 remove()

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.equals("b")) {
        it.remove();
    }
}

iterator.remove() 删完会把 expectedModCount 同步成新值,所以不炸。反过来 list.remove() 就不行。

遍历副本

先复制一份再遍历副本、删原集合,能绕开但没必要。

for (String s : new ArrayList<>(list)) {
    if (s.equals("b")) {
        list.remove(s);
    }
}

不推荐,额外开销大,还容易把「删哪个集合」搞混。

多线程场景换并发容器

真到了多个线程同时读写,fail-fast 集合本来就不合适,换成线程安全容器。

List<String> list = new CopyOnWriteArrayList<>();
Map<String, Integer> map = new ConcurrentHashMap<>();

CopyOnWriteArrayList 的迭代器是快照式的,不抛这个异常。ConcurrentHashMap 的迭代器是弱一致的,也不会抛。

思考过程

社区最常见的误判:看到 ConcurrentModificationException,先加 synchronized。结果单线程程序照样抛。

这个误区来自名字。官方文档开头就写了,这个异常不一定表示有别的线程在改。翻译过来就是,单线程违反对象契约也会抛。

另一个容易漏的点:stream().forEach(x -> list.remove(x)) 也是错的。stream 是给转换用的,不是给改集合用的。正确做法是 filter().collect()

还有 Map 的坑。遍历 keySet() 时调 map.remove(key) 会炸,遍历 entrySet() 时调 it.remove() 不炸,两者别混。

小结

  • ConcurrentModificationException 是 fail-fast 机制,不认线程,只认「遍历时改结构」。
  • 根子是 modCountexpectedModCount 对不上。
  • 首选 removeIf(),手动循环用 iterator.remove()
  • 真并发场景上 CopyOnWriteArrayListConcurrentHashMap
  • 别把它当可靠的并发检测手段,它可能漏报。

来源

最后更新于 2026-08-23

这篇帮到你了吗?

刚解决了一个棘手的报错?花两分钟记录下来,帮助下一个遇到同样问题的开发者。

贡献一条解法