ConcurrentModificationException源于迭代器的fail-fast机制:Itr构造时复制modCount为expectedModCount,后续next()/remove()前校验二者是否一致,不等则抛异常;该机制存在于ArrayList、HashMap等非线程安全集合中。

要真正理解 ConcurrentModificationException,不能只看异常信息或堆栈,得顺着源码链条一层层往下挖——从你写的那行 list.remove() 开始,到 ArrayList$Itr.checkForComodification() 抛出异常为止。这个过程不长,但每一步都藏着关键设计意图。
从 for-each 入口定位迭代器实现
增强 for 循环(如 for (String s : list))在编译后等价于显式调用 list.iterator(),返回的是 ArrayList 内部的私有迭代器类 Itr。这个类继承自 Iterator 接口,其核心方法 next() 和 remove() 都会触发校验逻辑。
-
ArrayList.iterator()返回 newItr(),构造时立即读取当前modCount并赋值给expectedModCount -
Itr是一个静态内部类,但它持有对外部ArrayList实例的隐式引用,能直接访问modCount - 所有结构性修改方法(
add()、remove(int)、clear()等)都会让modCount++,而set()或get()不会
追踪 checkForComodification 的触发点
每次调用 Itr.next() 或 Itr.remove() 前,都会先执行 checkForComodification() 方法。它的源码极简,却是一道“安全闸门”:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
注意两点:
立即学习“Java免费学习笔记(深入)”;
- 这个方法没有加锁、不涉及线程同步,纯粹是单次数值比对
- 它被设计成“被动检查”,不是在修改时拦截,而是在遍历下一步动作前主动断言——这就是 Fail-Fast 的本质:不预测冲突,只拒绝已发生的不一致
对比 modCount 与 expectedModCount 的生命周期
modCount 是定义在 AbstractList 中的 protected transient int 字段,被 ArrayList、LinkedList、HashMap 等广泛继承;expectedModCount 则是 Itr 类的实例字段,仅在迭代器创建时初始化一次,之后再无更新。
- 只要你在
Iterator活着期间调用过list.remove(),modCount就变了,但expectedModCount还是老样子 - 哪怕只删一个元素,下一次
next()调用就会失败——异常不是发生在remove()那一刻,而是在“继续遍历”的瞬间暴露 - 这也是为什么倒序 for 循环(
for (int i = size-1; i >= 0; i--))不会触发该异常:它没用Iterator,自然不走checkForComodification
延伸验证:看看 HashMap 的类似机制
别只盯着 ArrayList。打开 HashMap 源码,你会发现它的 HashIterator 同样维护 expectedModCount,且 keySet().iterator()、entrySet().iterator() 全部共享同一套校验逻辑。甚至 TreeMap 的 PrivateEntryIterator 也如此。这说明:这不是某个集合的 bug,而是整个非线程安全集合框架的统一契约。
- 想绕过?可以,但代价是放弃一致性保障(比如用
CopyOnWriteArrayList,它干脆不校验modCount,而是靠写时复制隔离读写) - 想调试?在
checkForComodification()打断点,观察modCount和expectedModCount的差值,就能精准定位哪次修改“动了地图”


















