ConcurrentModificationException 本质是迭代器检测到集合结构被意外修改,单线程遍历时调用 list.remove() 或 map.remove() 即可触发;其原理是 modCount 与 expectedModCount 不一致导致 checkForComodification() 抛异常。

ConcurrentModificationException 不是多线程专属错误,单线程遍历时直接调用 list.remove() 或 map.remove() 就会触发。它的本质是迭代器检测到集合结构被“意外修改”,而非并发竞争本身。
为什么 for-each 或 iterator.next() 时删元素就崩?
因为 for-each 底层就是 Iterator;而所有基于 modCount 机制的集合(ArrayList、HashMap、HashSet 等)在创建迭代器时,会把当前修改计数 modCount 快照为 expectedModCount。一旦你在遍历中调用 list.remove(),modCount 自增,但迭代器里的 expectedModCount 没变——下一次 next() 或 remove() 调用就会进 checkForComodification(),直接抛异常。
- 错误写法:
for (String s : list) { if (s.equals("x")) list.remove(s); } - 错误写法:
Iterator it = list.iterator(); while(it.hasNext()) { it.next(); list.remove(...); } - 正确前提:必须让“修改动作”和“迭代状态”同步更新
安全删除的三种实操路径
根据场景选对方法,别硬套“用 Iterator 就万事大吉”:
- 单线程遍历删元素 → 用
iterator.remove()(唯一推荐的遍历中删除方式) - 要边遍历边添加/替换 → 改用
ListIterator的add()或set()(Iterator不支持) - 需频繁读、偶发写(如监听列表、配置缓存)→ 换
CopyOnWriteArrayList,它写操作复制新数组,读不加锁,但注意内存开销和弱一致性(刚写的元素本次遍历看不到)
Map 遍历时删 key 的坑比 List 还深
HashMap 的 keySet().iterator() 同样受 modCount 约束;更隐蔽的是:哪怕你只删 entry.getValue(),只要触发了结构变化(比如扩容或树化),也可能连带影响迭代器。常见误操作:
- 错:
for (String k : map.keySet()) { if (k.startsWith("tmp")) map.remove(k); } - 错:
map.entrySet().forEach(e -> { if (e.getValue() == null) map.remove(e.getKey()); }); - 对:用
Iterator+> it = map.entrySet().iterator(); it.remove() - 对(JDK 8+):
map.entrySet().removeIf(e -> e.getValue() == null);(内部封装了安全迭代)
最容易被忽略的一点:这个异常不是“一定出在写线程”,而是出在“读线程里那个已过期的迭代器”。哪怕你确定没开多线程,只要在一次遍历中混用了集合原生修改方法,它就稳稳报给你看。










