ConcurrentModificationException本质是单线程结构一致性保护机制,非多线程专属;其核心通过modCount与expectedModCount校验实现,for-each中直接remove会因二者不一致而抛出异常。

ConcurrentModificationException(CME)不是“多线程专属”,而是一种单线程下也极易触发的结构一致性保护机制。它的核心目的不是阻止并发,而是防止迭代过程因集合结构变化导致行为错乱——比如跳过元素、重复访问、数组越界,甚至静默返回错误数据。
为什么遍历时删元素会报错?
Java 的 for-each 循环本质是调用 Iterator,而 ArrayList 等集合的迭代器在初始化时会记下当前的 modCount(结构修改计数器),并存为 expectedModCount。每次调用 next() 或 remove() 前,都会校验两者是否一致。
当你在 for-each 中写 list.remove(s):
- ArrayList 内部执行删除,
modCount自增 1 - 但迭代器里的
expectedModCount还是旧值 - 下一次
next()调用时触发checkForComodification(),发现不等 → 抛 CME
modCount 是什么?它怎么工作?
modCount 是定义在 AbstractList 中的 transient int 字段,所有非线程安全集合(ArrayList、LinkedList、HashMap 等)都继承它。它只在发生结构性修改时递增:
- 增:add、addAll、set(仅当索引等于 size 时可能扩容)
- 删:remove、removeAll、clear
- 改大小:ensureCapacity、trimToSize 等影响底层容器的操作
- ⚠️ 注意:仅修改元素内容(如
list.set(0, "X"))不会动 modCount
哪些写法能绕过 CME?原理各是什么?
关键不在于“能不能删”,而在于“谁来同步 modCount”:
-
用
iterator.remove():内部会调用集合删除 + 同步更新expectedModCount = modCount -
倒序 for 循环(
for (int i = list.size()-1; i >= 0; i--)):不依赖迭代器,无 expectedModCount 校验;删除后索引自然前移,不会漏判 -
removeIf():List 接口默认实现中,它自己管理迭代和删除逻辑,保证 modCount 同步 - CopyOnWriteArrayList:每次修改都新建数组,原迭代器始终遍历快照,所以永远不抛 CME(适合读多写少场景)
- Stream.filter().collect():不修改原集合,而是生成新集合,彻底规避结构冲突
别被名字误导:“Concurrent” 指的是逻辑冲突,不是线程数
即使只有主线程,只要“一边读(迭代)一边改(结构)”,就构成逻辑上的并发修改。Java 的 fail-fast 设计正是为了尽早暴露这种隐患——宁可中断,也不让程序带着不一致状态继续跑。这比出现偶发空指针、IndexOutOfBoundsException 或更难追踪的数据错位,要可靠得多。

















