fail-fast机制是Java集合的调试哨兵,非线程安全方案:遍历时若集合被结构性修改(add/remove/clear),立即抛ConcurrentModificationException;单线程下亦触发,仅与结构变更有关,与线程数无关。

Java 中的 fail-fast 机制不是线程安全方案,而是一种“主动报错”的设计:只要迭代过程中集合被结构性修改(比如 add、remove、clear),就立刻抛出 ConcurrentModificationException,强制你停下来检查问题。
为什么 foreach 循环删元素会崩?
因为 for (String s : list) 是语法糖,底层等价于用 Iterator 遍历。一旦你在循环体里调用 list.remove(s),就会触发 modCount 和 expectedModCount 不一致,立刻炸。
- 错误写法:
for (String s : list) { if ("aa".equals(s)) { list.remove(s); // ❌ 触发 ConcurrentModificationException } } - 正确写法(用迭代器自己的
remove()):Iterator
it = list.iterator(); while (it.hasNext()) { String s = it.next(); if ("aa".equals(s)) { it.remove(); // ✅ 安全:it 会同步更新 expectedModCount } }
单线程也会触发 fail-fast?
会,而且很常见。它和“多线程”无关,只和“谁改了结构”有关。哪怕只有你一个线程,只要遍历时用了集合自身的增删方法,就中招。
-
list.set(0, "new")不触发——只是改值,不改结构 -
list.add("x")、list.remove(0)、list.clear()全都触发——它们会让modCount++ - 注意:Vector 虽然线程安全,但它的
Iterator依然有fail-fast行为(内部也维护modCount)
怎么真正避免 ConcurrentModificationException?
别指望靠捕获异常来兜底,那说明逻辑已失控。真正靠谱的方式就三条:
立即学习“Java免费学习笔记(深入)”;
- 用
CopyOnWriteArrayList:适合读远多于写的场景,写操作复制数组,遍历永远看旧副本,完全不抛这个异常 - 收集后批量删:
List
toRemove = new ArrayList<>(); for (String s : list) { if (shouldRemove(s)) toRemove.add(s); } list.removeAll(toRemove); // ✅ 安全,遍历和修改分离 - 加锁同步:如果必须边遍历边改且不能换容器,就把整个遍历+修改块包进
synchronized(list)——但要注意锁对象必须是同一个,且性能代价明显
最常被忽略的一点是:fail-fast 的检测不是实时的。它只在调用 next() 或 hasNext() 时校验一次,所以异常可能延迟几轮才抛;另外,modCount 是 int,极端情况下溢出回绕(2147483647 → -2147483648)会导致校验失效——这说明它从来就不是并发控制手段,只是个调试哨兵。










