Java迭代器的快速失败机制通过modCount与expectedModCount比对实现:创建迭代器时复制modCount值到expectedModCount,每次next()或remove()前校验二者是否相等,不等则抛ConcurrentModificationException;该机制检测结构性修改,单线程下非迭代器方式修改集合也会触发,旨在尽早暴露逻辑错误而非提供线程安全。

Java 中集合迭代的“失效机制”其实不是真失效,而是快速失败(Fail-Fast)机制主动中断遍历——它不等程序跑出错结果,而是在检测到“结构被意外修改”时立刻抛出 ConcurrentModificationException。这个异常在单线程下频繁出现,恰恰说明它和“多线程并发”无关,核心是修改方式与遍历逻辑不匹配。
为什么单线程调用 list.remove() 就会触发异常
关键在于两个计数器的脱节:
- modCount:集合内部的“修改版本号”,每次 add、remove、clear 等结构性操作都会自增
- expectedModCount:迭代器创建时从 modCount 复制过来的快照值,代表“我预期集合结构没变”
- 每次调用
iterator.next()或iterator.remove()前,都会检查二者是否相等;不等就立即抛异常
当你用 for-each 遍历时,背后就是迭代器;但删元素却写 list.remove(x),这会让 modCount +1,而 expectedModCount 毫不知情——下一次 next() 校验失败,异常就来了。
哪些操作算“结构性修改”
只有真正改变集合大小或底层结构的操作才会动 modCount:
- 会触发异常的操作:
add()、remove()、clear()、addAll()、retainAll()、removeIf()(注意:这个方法本身是安全的,它内部用的是迭代器逻辑) - 不会触发的操作:
get()、set()、contains()—— 它们不改结构,只读或替换元素
单线程下安全删除的常用方式
不绕开迭代器,就能避开这个机制的拦截:
- 用迭代器自己的
remove()方法:iterator.remove()—— 它会在删完后同步更新 expectedModCount - 用 Java 8 的
removeIf():list.removeIf(x -> x.isExpired())—— 底层封装了安全的迭代删除逻辑 - 反向 for 循环(仅限 List):
for (int i = list.size()-1; i >= 0; i--) { if (...) list.remove(i); }—— 从后往前删,索引不会错乱,也不走迭代器 - 先收集待删元素,再批量移除:
ListtoRemove = new ArrayList(); for (T t : list) if (...) toRemove.add(t); list.removeAll(toRemove);
别被名字误导:“Concurrent” 不等于 “多线程”
这个异常名里的 “Concurrent” 指的是多个行为在时间上重叠且逻辑冲突,比如“正在遍历”和“同时删元素”这两个动作在语义上不该共存。JVM 不关心是不是同一个线程,只看操作路径是否绕过了迭代器契约。所以哪怕代码里没开新线程,只要违反了“用谁遍历,就用谁修改”的原则,就会被 Fail-Fast 拦下。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











