ConcurrentModificationException 在单线程遍历时调用集合结构性修改方法(如list.remove())即触发,因迭代器的expectedModCount与集合modCount不匹配;多线程下应选用CopyOnWriteArrayList或ConcurrentHashMap等fail-safe容器。

ConcurrentModificationException 是怎么被触发的
它不是“并发”专属异常,而是集合结构被意外修改时的即时警报。核心就一句话:modCount 和 expectedModCount 对不上了。前者是集合自己记的“改过几次”,后者是迭代器创建时抄下来的快照。只要遍历中途有人调了 add、remove、clear 这类结构性方法(哪怕单线程),下次调 next() 就会进 checkForComodification(),然后直接抛 ConcurrentModificationException。
- for-each 循环本质就是
iterator()+while(hasNext()),所以一样中招 -
Enumeration不触发,因为它压根没remove方法,也不维护expectedModCount - 这个检测是“尽力而为”——JVM 不保证 100% 捕获所有并发修改,所以不能靠它做同步逻辑
单线程里为什么也会 fail-fast
很多人以为只有多线程才出问题,其实最常见场景就在自己写的 for-each 里删元素:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : list) {
if ("b".equals(s)) {
list.remove(s); // ⚠️ 这里就炸了
}
}
原因很简单:for-each 底层用的是迭代器,但你删元素用的是 list.remove(),它只更新了 modCount,没告诉迭代器——等下一轮 next() 调用时,校验失败,立刻中断。
- 正确做法是用迭代器自己的
remove()方法,它会同步更新expectedModCount - 如果要删多个,别边遍历边删;先收集待删项,再用
removeAll() - 用普通 for 循环从后往前删(
i--)能绕过异常,但逻辑易错,不推荐作为首选方案
多线程下怎么安全地遍历并修改 ArrayList
别硬扛 ArrayList + 手动加锁——性能差、易死锁、还容易漏锁点。真要并发读写,换容器比修逻辑更靠谱:
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
立即学习“Java免费学习笔记(深入)”;
- 读多写少:用
CopyOnWriteArrayList,写操作复制数组,遍历永远看到快照,不抛异常,但内存开销大、写延迟高 - 读写均衡:用
ConcurrentHashMap替代HashMap,它分段锁 + CAS,无modCount,天然不 fail-fast - 必须用原生
ArrayList?那就用Collections.synchronizedList(),但注意:迭代仍需手动同步整个块,否则还是可能出问题
fail-fast 和 fail-safe 的关键区别在哪
根本不在“快不快”,而在“看谁的数据”:
- fail-fast(如
ArrayList.iterator()):盯着原始集合的modCount,改了就停,宁可中断也不读脏数据 - fail-safe(如
ConcurrentHashMap.keySet().iterator()或CopyOnWriteArrayList.iterator()):遍历的是快照或副本,原集合怎么改都影响不到当前迭代,但你也看不到那些新改的内容 - 没有银弹:fail-safe 避开了异常,却带来数据可见性问题;fail-fast 暴露了问题,但要求你主动处理结构变更逻辑
最容易被忽略的一点:很多开发者以为换成 ConcurrentHashMap 就万事大吉,但它对 keySet() 迭代器是 fail-safe,对 entrySet().iterator() 却仍是弱一致性——删掉的 entry 可能还在迭代中出现一次,这不是 bug,是设计使然。

















