2025.4.27
为什么多线程并发操作 ArrayList 会报 ConcurrentModificationException?
ConcurrentModificationException
是 Java 集合框架中的一个常见错误,特别是在多线程环境下操作 ArrayList
时。这个异常的根本原因是 "快速失败"(fail-fast)机制 检测到了并发修改。
底层机制解析
1. ArrayList 的非线程安全设计
ArrayList 的设计初衷是用于单线程环境,它没有内置任何同步机制。当多个线程同时操作同一个 ArrayList 时:
- 一个线程可能在遍历集合(使用迭代器)
- 另一个线程同时修改集合(添加/删除元素)
2. 快速失败(Fail-Fast)机制
ArrayList 的迭代器实现了 fail-fast 机制,通过维护一个 modCount
(修改计数器)来实现:
// ArrayList 中的关键字段
protected transient int modCount = 0; // 记录结构性修改次数
每次对 ArrayList 进行结构性修改(add/remove等)时,modCount
都会递增。
3. 迭代过程中的检查
当创建迭代器时,它会记录当前的 modCount
值:
// ArrayList.Itr 内部类
int expectedModCount = modCount; // 初始化时记录当前修改计数
每次调用 next()
或 remove()
时,迭代器会检查:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
4. 多线程冲突场景
线程A(迭代) 线程B(修改)
│ │
├─ 创建迭代器 │
│ expectedModCount = 5 │
│ ├─ 添加元素 → modCount++ (6)
├─ 调用next() │
│ 检查 6 != 5 → 抛出异常 │
为什么 Vector 不会出现这个问题?
Vector 是线程安全的,因为:
- 所有方法都用
synchronized
修饰 - 迭代器也会在同步块中操作
- 虽然也有 modCount 机制,但同步保证了修改的原子性
解决方案对比
方案 | 优点 | 缺点 |
---|---|---|
Collections.synchronizedList |
简单易用 | 全局锁性能较低 |
CopyOnWriteArrayList |
读操作完全无锁 | 写操作性能较差,适合读多写少 |
显式同步(synchronized块) | 控制粒度更细 | 需要手动管理,容易出错 |
最佳实践:在大多数现代Java应用中,CopyOnWriteArrayList
是最佳选择,除非有特别高的写性能要求。