ConcurrentModificationException 根本原因是 modCount 与 expectedModCount 不一致,单线程遍历时修改集合也会触发;正确删除方式为 Iterator.remove()、倒序索引删除或收集后 removeAll()。

ConcurrentModificationException 出现的根本原因
它不是因为“多线程并发修改”才触发的——单线程遍历中修改集合也会抛出。核心在于 modCount 和 expectedModCount 不一致:集合内部维护一个修改计数器 modCount,而迭代器(如 ArrayList$Itr)在创建时会拷贝一份到 expectedModCount;后续每次调用 next() 或 remove() 前,都会校验二者是否相等。
哪些操作会触发 CME(常见错误场景)
以下都是典型单线程下也稳稳报错的写法:
- 用
for-each遍历时调用集合自身的remove()或add() - 用
Iterator遍历时,调用集合对象的remove()(而非iterator.remove()) - 多个迭代器共享同一集合,其中一个修改后,另一个继续
next() -
CopyOnWriteArrayList不会抛 CME,但它的迭代器是快照式,看不到后续添加的元素——这常被误认为“修复了问题”,其实只是语义不同
安全删除元素的三种正确方式
关键原则:删除动作必须由当前迭代器发起,或避开迭代器机制:
- 用
Iterator.remove()—— 唯一允许在遍历时由迭代器自己删元素的方式:Iterator
it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (s.isEmpty()) it.remove(); // ✅ 安全 } - 倒序
for循环 + 索引删除:for (int i = list.size() - 1; i >= 0; i--) { if (list.get(i).isEmpty()) list.remove(i); // ✅ 安全(索引不会因前面删除而错位) } - 收集待删元素,遍历结束后统一调用
removeAll():List
toRemove = new ArrayList<>(); for (String s : list) { if (s.isEmpty()) toRemove.add(s); } list.removeAll(toRemove); // ✅ 安全,但注意 equals 实现是否合理
为什么增强 for 循环无法直接删除
因为 for-each 底层就是用 Iterator 实现的,且不暴露迭代器引用。编译后等价于:
立即学习“Java免费学习笔记(深入)”;
for (Iteratorit = list.iterator(); it.hasNext(); ) { String s = it.next(); // ← 这里隐式调用 next() if (s.isEmpty()) list.remove(s); // ← 这里修改了 modCount,但 it 仍持有旧 expectedModCount }
下次循环进入 it.hasNext() 时就会校验失败,抛出 ConcurrentModificationException。别指望编译器帮你绕过这个检查——它正是为了防止逻辑错乱而存在的。
真正容易被忽略的是:某些工具类(如 Collections.synchronizedList())只保证单个方法线程安全,并不解决迭代过程中的结构性修改检测。CME 的本质是 fail-fast 机制,不是并发控制机制;想彻底规避,得换数据结构(如 ConcurrentHashMap、CopyOnWriteArrayList),但必须清楚它们的读写语义代价。










