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 是线程安全的,因为:

  1. 所有方法都用 synchronized 修饰
  2. 迭代器也会在同步块中操作
  3. 虽然也有 modCount 机制,但同步保证了修改的原子性

解决方案对比

方案 优点 缺点
Collections.synchronizedList 简单易用 全局锁性能较低
CopyOnWriteArrayList 读操作完全无锁 写操作性能较差,适合读多写少
显式同步(synchronized块) 控制粒度更细 需要手动管理,容易出错

最佳实践:在大多数现代Java应用中,CopyOnWriteArrayList 是最佳选择,除非有特别高的写性能要求。

posted @ 2025-04-27 23:21  258333  阅读(11)  评论(0)    收藏  举报