StackTraces
Java已驗證

Java ConcurrentModificationException 單執行緒也會觸發

@debug_master32 天前更新閱讀 6 min00

一句話先說結論:這個例外的名字有誤導性。它不要求多執行緒,單執行緒裡一邊遍歷一邊改集合就會拋。根子是 fail-fast 迭代器拿 modCount 和 expectedModCount 對不上。修法就兩條:用 iterator.remove(),或者 Java 8 的 removeIf()。

背景

ConcurrentModificationException 是 Java 集合框架裡最常見的運行時例外之一。名字裡帶「併發」,但它跟多執行緒沒有必然關係。

ArrayList、HashMap、HashSet 這些標準集合都用 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)
    at com.example.Main.main(Main.java:9)

HashMap 也一樣,遍歷 entrySet 時刪 key:

java.util.ConcurrentModificationException
    at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1597)
    at java.base/java.util.HashMap$EntryIterator.next(HashMap.java:1730)
    at com.example.Main.cleanup(Main.java:24)
    at com.example.Main.main(Main.java:8)

注意堆疊落在 next(),不是你的業務程式碼那行。很多人第一反應去 process() 找問題,實際根子在更早的遍歷寫法上。

根因分析

每個集合內部維護一個 modCount 計數器,每次結構性修改就加 1。

迭代器創建時,把當時的 modCount 記作 expectedModCount。之後每次 next() 都校驗一次,兩個值不一致就拋例外。

ArrayList 的 Itr 大致是這麼寫的:

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 機制,不認執行緒,只認「遍歷時改結構」。
  • 根子是 modCount 和 expectedModCount 對不上。
  • 首選 removeIf(),手動循環用 iterator.remove()。
  • 真併發場景上 CopyOnWriteArrayList 或 ConcurrentHashMap。
  • 別把它當可靠的併發檢測手段,它可能漏報。

References(參考來源)

最後更新於 2026-09-09

這篇幫到你了嗎?

剛解決了一個棘手的報錯?花兩分鐘記錄下來,幫助下一個遇到同樣問題的開發者。

貢獻一條解法