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排查:ConcurrentModificationException 并发修改异常深度解析
> Java 开发中最常见的运行时异常之一——`ConcurrentModificationException`,几乎每个 Java 程序员都踩过这个坑。它看似与多线程并发相关,实则单线程下也会频繁触发。本文深入剖析其根因、复现路径与三种修复方案。  ## 什么是 ConcurrentModificationException? `ConcurrentModificationException` 是 Java 集合框架中的 **fail-fast(快速失败)** 机制的产物。当迭代器遍历集合时,如果检测到集合的结构被修改(增删元素),而迭代器自身不知情,就会抛出此异常。 关键点:**这个名字有误导性**。虽然带 "Concurrent" 前缀,但单线程下对集合的 `remove/add` 操作同样会触发,这是最常见的误解。 ## 根因分析:modCount 机制 Java 的 `ArrayList`、`HashMap` 等非线程安全集合内部维护一个 `modCount` 字段,记录集合结构修改的次数。获取迭代器时,迭代器会记录当前的 `modCount` 为 `expectedModCount`。每次调用 `next()` 时,都会检查两者是否一致: ```java // ArrayList.Itr 源码(JDK 8+) final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } ```  **单线程 Bug 根因**:使用 `list.remove()` 而非 `iterator.remove()` 时,`modCount` 被更新,但迭代器内部的 `expectedModCount` 未同步更新,下次 `next()` 检查时两者不一致,抛出异常。 **多线程 Bug 根因**:线程 A 在遍历,线程 B 修改了集合结构,`modCount` 变化后线程 A 的迭代器检测到不一致。 ## 复现代码 ### 单线程复现 ```java import java.util.ArrayList; import java.util.List; public class BugDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("java"); list.add("bug"); list.add("kotlin"); // 经典陷阱:foreach + list.remove() for (String s : list) { if (s.equals("bug")) { list.remove(s); // 💥 ConcurrentModificationException! } } } } ``` `for-each` 循环语法糖底层就是 Iterator,所以等价于: ```java Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); // 这里 checkForComodification() 失败 if (s.equals("bug")) { list.remove(s); // 只更新了 modCount,没更新 expectedModCount } } ``` ### 多线程复现 ```java import java.util.*; import java.util.concurrent.*; public class ConcurrentBugDemo { public static void main(String[] args) throws Exception { List<String> list = new ArrayList<>(); for (int i = 0; i < 100; i++) list.add("item-" + i); ExecutorService pool = Executors.newFixedThreadPool(2); pool.submit(() -> { for (String s : list) { System.out.println(s); // 遍历 } }); pool.submit(() -> { list.removeIf(s -> s.contains("5")); // 并发修改 }); pool.shutdown(); pool.awaitTermination(5, TimeUnit.SECONDS); } } ``` ## 修复方案  ### 方案1:使用 Iterator.remove()(单线程推荐) ```java Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (s.equals("bug")) { it.remove(); // ✓ 迭代器内部同步更新 expectedModCount } } ``` `Iterator.remove()` 会在删除元素后将 `expectedModCount` 同步为最新的 `modCount`,保证后续遍历正常。 ### 方案2:使用 CopyOnWriteArrayList(多线程推荐) ```java import java.util.concurrent.CopyOnWriteArrayList; List<String> list = new CopyOnWriteArrayList<>(); list.add("java"); list.add("bug"); list.add("kotlin"); for (String s : list) { if (s.equals("bug")) { list.remove(s); // ✓ 写时复制,迭代器快照不受影响 } } ``` `CopyOnWriteArrayList` 的迭代器基于创建时的数组快照,写操作会复制新数组,不影响正在遍历的迭代器。**代价**:每次写操作都复制整个数组,内存开销大,适合读多写少场景。 ### 方案3:使用 removeIf(Java 8+,最简洁) ```java list.removeIf(s -> s.equals("bug")); // ✓ 内部使用 Iterator.remove() ``` 这是单线程下的最佳选择——简洁、安全、可读。 ## 避坑总结 | 场景 | 推荐方案 | 原因 | |------|---------|------| | 单线程删除 | `removeIf` / `Iterator.remove()` | 同步 expectedModCount | | 多线程遍历+修改 | `CopyOnWriteArrayList` | 读写分离,线程安全 | | 多线程遍历只读 | `Collections.unmodifiableList` | 不可变,无修改风险 | | 需要索引删除 | 倒序遍历 `for (int i=size-1; i>=0; i--)` | 避免索引错位 | **核心原则**:谁遍历,谁修改。迭代器遍历就必须用迭代器的方法修改,不要绕过迭代器直接操作集合。
admin
2026年5月17日 00:06
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
PDF文档(打印)
分享
链接
类型
密码
更新密码