ConcurrentModificationException由modCount与expectedModCount不一致触发,体现fail-fast机制;单线程遍历中直接调用集合remove()也会触发,安全删除应使用Iterator.remove()、removeIf()或批量移除。

ConcurrentModificationException 是怎么被触发的
Java 的 ArrayList、HashMap 等非线程安全集合,内部维护一个 modCount 计数器。每次结构修改(如 add()、remove())都会递增它;而迭代器在创建时会记录当时的 expectedModCount。遍历时只要发现两者不一致,就立刻抛出 ConcurrentModificationException —— 这是「快速失败」(fail-fast)机制,不是并发安全保证,只是检测到“可能出错”就报错。
注意:这个异常不一定发生在多线程场景下。单线程里边遍历边用集合自身方法删元素,一样会触发:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : list) {
if ("b".equals(s)) list.remove(s); // ⚠️ 这里直接调用 list.remove() 就会爆
}
单线程遍历时安全删除的三种写法
核心原则:别让迭代器和集合的修改操作混在一起。推荐以下方式:
- 用
Iterator.remove()—— 唯一被设计为遍历时安全删除的方式:Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if ("b".equals(s)) it.remove(); // ✅ 合法,会同步更新 expectedModCount } - 用
removeIf()(Java 8+)—— 内部封装了安全遍历逻辑:list.removeIf("b"::equals); // ✅ 简洁且安全 - 收集待删元素,遍历结束后批量删(适合条件复杂或需多次判断):
List<String> toRemove = new ArrayList<>(); for (String s : list) { if (needRemove(s)) toRemove.add(s); } list.removeAll(toRemove); // ✅ 避开遍历期修改
多线程环境下该选哪个集合
如果真有多个线程同时读写,不能只靠规避异常,得换线程安全的实现:
CopyOnWriteArrayList:适合读多写少,每次写操作复制整个数组,迭代器基于快照,不会抛ConcurrentModificationException,但内存和写性能代价大ConcurrentHashMap:支持高并发读写,keySet()/values()返回的集合是弱一致性视图,遍历时不会因其他线程修改而报错(但可能看不到最新修改)- 慎用
Collections.synchronizedList():它只给每个方法加synchronized,但迭代仍是复合操作(hasNext()+next()),仍需手动同步整个遍历块:synchronized (list) { for (String s : list) { ... } }
为什么增强 for 循环无法捕获 ConcurrentModificationException
增强 for 循环底层就是用 Iterator,但它把 next() 和异常处理全包在字节码里,你没法在循环体里 catch 它 —— 异常会直接跳出循环并向上抛。想兜底?只能在外层 try/catch,但这掩盖了根本问题:说明你的遍历逻辑本身存在结构性风险。
真正要警惕的,不是“怎么 catch 这个异常”,而是“为什么会在遍历中途改集合”。哪怕用 CopyOnWriteArrayList 避开了异常,若业务上要求强实时一致性,它返回的旧快照反而可能引入更隐蔽的 bug。