ConcurrentModificationException 并发修改异常详解
一、异常基本概念
ConcurrentModificationException 是 Java 开发中常见的运行时异常,属于 java.util 包。它通常在集合遍历过程中被修改时抛出,无论是单线程还是多线程环境都可能发生。
二、异常发生的核心场景
- 单线程场景:在遍历集合时直接修改集合内容
- 多线程场景:一个线程遍历集合,另一个线程修改集合
三、底层机制:fail-fast 机制
Java 集合框架(如 ArrayList、HashMap 等)普遍采用 fail-fast 机制来检测并发修改,其核心原理是:
- 每个集合内部维护一个 modCount 变量,记录集合的修改次数
- 迭代器在创建时会保存当前的 modCount 到 expectedModCount
- 每次迭代操作(如 next())时,会检查 modCount 是否等于 expectedModCount
- 如果不等,说明集合被修改,抛出 ConcurrentModificationException
四、典型代码示例
单线程场景示例
import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class ConcurrentModificationDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("A"); list.add("B"); list.add("C");
// 错误做法:遍历过程中直接删除元素 for (String item : list) { if ("B".equals(item)) { list.remove(item); // 这里会抛出ConcurrentModificationException } }
// 正确做法:使用迭代器的remove方法 Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if ("B".equals(item)) { iterator.remove(); // 安全删除 } }
// Java 8+ 推荐做法:使用Stream过滤 list = list.stream() .filter(item -> !"B".equals(item)) .collect(java.util.stream.Collectors.toList()); } } |
多线程场景示例
import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; public class MultiThreadModificationDemo { private static List<String> list = new ArrayList<>();
public static void main(String[] args) { list.add("A"); list.add("B"); list.add("C");
// 线程1:遍历集合 Thread reader = new Thread(() -> { for (String item : list) { try { TimeUnit.MILLISECONDS.sleep(100); System.out.println("读取元素: " + item); } catch (InterruptedException e) { e.printStackTrace(); } } });
// 线程2:修改集合 Thread writer = new Thread(() -> { try { TimeUnit.MILLISECONDS.sleep(300); System.out.println("删除元素: B"); list.remove("B"); // 线程2修改集合,线程1会抛出异常 } catch (InterruptedException e) { e.printStackTrace(); } });
reader.start(); writer.start(); } } |
五、常见集合的并发修改行为
- ArrayList / LinkedList:
- 不支持并发修改,遍历时修改会触发 ConcurrentModificationException
- 单线程中可通过迭代器 remove() 方法安全修改
- HashMap / HashSet:
- 同样基于 fail-fast 机制,遍历时修改会抛出异常
- foreach 遍历、迭代器遍历、keySet()/values()/entrySet() 遍历时修改都会触发异常
- CopyOnWriteArrayList:
- 采用 fail-safe 机制,内部通过复制数组实现
- 遍历时允许修改,修改会创建新数组,遍历使用旧数组
- 适用于读多写少的场景,但写操作开销较大
- ConcurrentHashMap:
- Java 7 采用分段锁(Segment),Java 8 采用红黑树 + CAS
- 支持并发读写,遍历过程中修改不会抛出异常
- 是线程安全的高性能哈希表
六、解决方案与最佳实践
1. 单线程场景解决方案
- 使用迭代器的 remove 方法:
Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if (需要删除) { iterator.remove(); } } |
- 使用 Stream API 过滤(Java 8+):
list = list.stream() .filter(item -> 过滤条件) .collect(Collectors.toList()); |
- 遍历前复制集合:
for (String item : new ArrayList<>(list)) { // 操作item,不影响原集合 } |
2. 多线程场景解决方案
- 使用并发安全的集合:
- 列表:CopyOnWriteArrayList
- 哈希表:ConcurrentHashMap
- 集合:CopyOnWriteArraySet
- 加锁保护:
List<String> list = new ArrayList<>(); Object lock = new Object(); // 读操作 synchronized (lock) { for (String item : list) { // 遍历操作 } } // 写操作 synchronized (lock) { list.add("新元素"); } |
- 使用 Collections.synchronizedXXX 包装:
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>()); |
七、fail-fast 与 fail-safe 的区别
特性 | fail-fast(如 ArrayList) | fail-safe(如 CopyOnWriteArrayList) |
检测机制 | 遍历过程中检测集合修改 | 遍历使用集合的副本,不检测原集合修改 |
异常情况 | 发现修改立即抛出 ConcurrentModificationException | 不会抛出异常,遍历旧数据 |
性能开销 | 开销小,无需复制集合 | 写操作需要复制集合,开销较大 |
适用场景 | 单线程或不允许并发修改的场景 | 多线程读多写少的场景 |
八、总结
ConcurrentModificationException 是 Java 集合框架中用于保护数据一致性的机制,本质是 fail-fast 策略的体现。避免该异常的核心原则是:
- 单线程环境:遍历时使用迭代器的 remove() 方法,或使用 Stream 过滤
- 多线程环境:使用并发安全的集合(如 CopyOnWriteArrayList、ConcurrentHashMap)
- 理解集合的线程安全特性,根据场景选择合适的数据结构
掌握这些知识后,能有效避免并发修改异常,写出更健壮的 Java 代码。