最近也是在学习Go语言,然后发现Go没有这个机制因此你如果for...range和s = append(s[:i], s[i+1:]...)这种搭配起来就会出现错误,而Go不会显式的抛出这个错误需要你自己Debug出来,当然可能主要是Go的语言哲学主要是轻量,不会有这种检查机制
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
for (Integer i : list) { // 增强 for,底层是迭代器
if (i % 2 == 0) {
list.remove(i); // ❌ 用集合本身的方法删除
}
}
// 抛出 java.util.ConcurrentModificationException
单线程也会抛这个异常!如果没有这个fail-fast机制那么就会:
| 步骤 | cursor | 数组实际状态 | next() 返回 | 结果 |
|---|---|---|---|---|
| 1 | 0→1 | [1,2,3,4] | 1 | 奇数,保留 |
| 2 | 1→2 | [1,2,3,4] | 2 | 偶数 → 删!数组变 [1,3,4],但 cursor 仍 = 2 |
| 3 | 2→3 | [1,3,4] | 4 | ⚠️ 元素 3 被跳过了! |
这样就会导致迭代器里面的元素被跳过啥的,就会导致遍历不完全的,因此Java不允许这样的事情发生就有了这个机制。
删掉 2 后,3 从下标 2 挪到了下标 1,而 cursor 已经走到 2,于是直接读到了 4——元素 3 被静默漏掉。这种「算错但不报错」的 bug 最难排查。fail-fast(下一节的 modCount)就是为了把它从「静默算错」变成「当场抛异常」。
fail-fast机制实现的原理
增强for循环的本质其实就是基于迭代器的语法糖,
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
Integer i = it.next(); // ← 异常在 next() 里抛出
if (i % 2 == 0) {
list.remove(i);
}
}
而这个异常的来源也是Integer i = it.next();这里抛出来的错误
fail-fast 靠 ArrayList 内部维护的一个计数器 modCount(modification count,记录结构性修改次数,如 add/remove)。
- 调用
list.iterator()时,迭代器把当前的modCount存进自己的expectedModCount。 - 每次
it.next()都会调用checkForComodification()检查两者是否相等。 list.remove()会让modCount++,但不会更新迭代器的expectedModCount→ 两者不等 → 抛异常。
// ArrayList 源码(简化)
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
public E next() {
checkForComodification(); // 每次取元素都校验
// ... 返回元素
}
为什么不可以使用list.remove()这种方法呢?
关键点:expectedModCount 不是 ArrayList 的字段,而是迭代器(Itr)自己的私有字段。ArrayList 压根不知道它的存在,自然也改不了它。但是如果是迭代器自己去进行操作那么对应的索引就会自动对其,那么你就可以进行操作(是按照迭代器创建的时候的快照对其的)
| 变量 | 属于谁 | 作用 |
|---|---|---|
modCount | ArrayList(外部对象) | 记录集合被结构性修改的次数 |
expectedModCount | Iterator(迭代器实例,ArrayList 的内部类对象) | 迭代器创建那一刻,对 modCount 拍的「快照」 |
// list.remove():只改自己的 modCount,够不到迭代器的 expectedModCount
public boolean remove(Object o) {
...
modCount++; // ✅ 只动自己,没有任何一行去碰 expectedModCount
}
// it.remove():是"自己人",删完顺手把自己的快照也对齐
public void remove() {
ArrayList.this.remove(lastRet); // 调用外部类删除,modCount++(如变成 5)
expectedModCount = modCount; // ✅ 自己同步,让两者重新相等
}
三层原因:
①够不到——list.remove() 执行时手里没有任何迭代器对象的引用,改不了它的私有字段;
②不知道改哪个——同一个 list 可同时存在多个迭代器,而 ArrayList 根本没维护「我有哪些迭代器」的列表;
③这就是本意——fail-fast 就是要让「绕过迭代器直接改集合」被发现并报错,如果list.remove() 真去同步了快照,fail-fast 反而失效了。
fail-fast 不能「防止」并发问题,而且主要是单线程中防止
fail-fast 不是线程安全机制,也不能解决并发问题——它防不住,也没打算防。它只是一个「尽力而为的检测器」。
根因还是一个细节:modCount 不是 volatile。
- 真正的多线程场景下,一个线程对
modCount++的修改不保证对另一个线程立即可见(可能停在 CPU 缓存里)→ CME 可能漏检,也可能在不该抛时抛出。 - 因此JDK 官方文档明确说:fail-fast 行为无法保证,绝不能编写依赖此异常来保证正确性的程序,它只应用于发现 bug。
- 它真正的价值:在「边遍历边改」这种写错的代码(尤其单线程误用)中尽早暴露问题,而不是默默返回错乱数据让 bug 潜伏。
正确的删除方式
方式 A:用迭代器自己的 remove()(它会同步更新 expectedModCount)
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
if (it.next() % 2 == 0) {
it.remove(); // ✅ 内部会执行 expectedModCount = modCount
}
}
方式 B:removeIf()(JDK 8+,最简洁)
list.removeIf(i -> i % 2 == 0); // ✅ 推荐
和Go的对比与思考
Go 没有 Java 的 fail-fast(无 modCount / CME)。
Go 的range 不是对象,它是语言关键字,编译后就是一个朴素的下标/指针循环。slice 本身只是个轻量值(指针 + len + cap 三个字段),没有任何隐藏的簿记字段,也没有一个"迭代器对象"能让你往里塞 modCount。
而没有迭代器对象因此Go的for..range会更轻量但是也会导致没有这种提醒机制来防止你不小心写错代码,还是性能和语言哲学的取舍吧
6431

被折叠的 条评论
为什么被折叠?



