Bug排查
Python 可变默认参数陷阱:一个隐蔽的 Bug
Java NullPointerException 深度排查指南
常见 Bug 排查——Python 中 mutable default argument 陷阱(可变默认参数导致的共享状态Bug)
Java Bug排查:ConcurrentModificationException 并发修改异常深度解析
Java 并发 Bug - ConcurrentModificationException 排查与修复
Java 并发 Bug:ArrayList 的 ConcurrentModificationException
Python 可变默认参数陷阱:一次踩坑与彻底修复
本站点使用 MrDoc 构建
-
+
Java 并发 Bug:ArrayList 的 ConcurrentModificationException
> 分类:Bug 排查 | 语言:Java | 严重程度:🔴 高  ## Bug 现象 在使用 `for-each` 循环遍历 `ArrayList` 并调用 `list.remove()` 时,抛出异常: ``` Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909) at java.util.ArrayList$Itr.next(ArrayList.java:859) at com.example.BugDemo.main(BugDemo.java:15) ``` **现象描述**:程序在遍历集合时尝试删除元素,随即崩溃。单线程下也会发生——这并非"并发"问题,而是 **fail-fast 机制**的保护性检查。 ## 复现步骤 1. 创建一个 `ArrayList` 并添加若干元素 2. 使用 `for-each`(语法糖 → `Iterator`)遍历 3. 在循环体内调用 `list.remove(element)` 4. 循环进入下一次 `iterator.next()` 时触发异常 ```java List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D")); for (String s : list) { if ("B".equals(s)) { list.remove(s); // 💥 Boom! } } ```  ## 根因分析 ### Fail-Fast 机制 `ArrayList` 内部维护两个计数器: | 字段 | 位置 | 含义 | |------|------|------| | `modCount` | `ArrayList` | **结构修改次数**(add/remove/inc) | | `expectedModCount` | `Itr`(内部迭代器) | 迭代器创建时记录的 `modCount` 快照 | 每次 `iterator.next()` 调用时,会先执行 `checkForComodification()`: ```java final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } ``` **关键时序**: 1. 迭代器创建 → `expectedModCount = modCount`(假设 = 4) 2. 循环中 `list.remove()` → `modCount++`(变为 5) 3. 下一次 `next()` → `modCount(5) != expectedModCount(4)` → 💥 ### 为什么叫 "Concurrent"ModificationException? 虽然名字暗示并发,但单线程中的结构性修改同样会触发。Java 集合的 fail-fast 设计目标是 **尽早暴露** 不一致的修改行为,而非仅在多线程场景下保护。 ### 特殊情况:恰好删除倒数第二个元素 如果删除的是倒数第二个元素,`hasNext()` 会提前返回 `false`,跳过最后的 `next()` 检查,**不会抛异常**。这是一个容易误导的边界 case: ```java List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C")); for (String s : list) { if ("B".equals(s)) list.remove(s); // 倒数第二个,不报错! } // list = ["A", "C"],静默成功 ``` ## 修复方案  ### 方案 1:使用 Iterator.remove()(推荐) ```java Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if ("B".equals(s)) { it.remove(); // ✅ 安全删除,同步更新 expectedModCount } } ``` **原理**:`Iterator.remove()` 会在删除后将 `expectedModCount` 同步为最新的 `modCount`,下次检查不会报错。 ### 方案 2:使用 CopyOnWriteArrayList ```java List<String> list = new CopyOnWriteArrayList<>(Arrays.asList("A", "B", "C", "D")); for (String s : list) { if ("B".equals(s)) { list.remove(s); // ✅ 线程安全,迭代的是快照 } } ``` **原理**:`CopyOnWriteArrayList` 的迭代器基于创建时的内部数组快照,写操作复制新数组,不影响正在进行的迭代。 **注意**:每次写操作都复制整个数组,适合 **读多写极少** 的场景。 ### 方案 3:使用 removeIf()(Java 8+,最简洁) ```java list.removeIf(s -> "B".equals(s)); // ✅ 内部使用 Iterator.remove() ``` **原理**:`removeIf` 内部实现等价于方案 1 的迭代器删除,但语法更简洁。 ### 方案对比 | 方案 | 线程安全 | 性能 | 代码简洁度 | 适用场景 | |------|---------|------|-----------|---------| | `Iterator.remove()` | ❌ 单线程 | ⭐⭐⭐ | ⭐⭐ | 通用 | | `CopyOnWriteArrayList` | ✅ | ⭐(写开销大) | ⭐⭐⭐ | 读多写少、并发 | | `removeIf()` | ❌ 单线程 | ⭐⭐⭐ | ⭐⭐⭐⭐ | Java 8+ | ## 预防措施 1. **遍历时不要直接调用集合的 add/remove/clear**,始终通过迭代器操作 2. 代码审查时重点检查 `for-each` 循环内的集合修改操作 3. 启用静态分析工具(SpotBugs、Error Prone)检测此类模式 4. 并发场景优先选择 `CopyOnWriteArrayList` 或 `ConcurrentHashMap` 5. 使用 `Collections.unmodifiableList()` 包装不需要修改的集合,从源头禁止修改 ## 扩展阅读 - [JDK ArrayList 源码](https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/util/ArrayList.java) - [Java Fail-Fast 机制详解](https://docs.oracle.com/javase/8/docs/api/java/util/ConcurrentModificationException.html) - [Effective Java - Item 46: Prefer side-effect-free functions in streams](https://www.oreilly.com/library/view/effective-java/9780134686097/)
admin
2026年5月17日 05:14
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
PDF文档(打印)
分享
链接
类型
密码
更新密码