🌺The Begin🌺点点关注,收藏不迷路🌺

1. 引言:一个让无数程序员抓狂的异常

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

for (String s : list) {
    if ("B".equals(s)) {
        list.remove(s);  // ❌ 抛出 ConcurrentModificationException
    }
}

有多少人写过类似的代码?又有多少次被这个突如其来的异常搞得手足无措?

本文将深入剖析 ConcurrentModificationException 的产生原理、触发场景以及最佳解决方案。

2. 一图看懂:异常产生的完整流程

有变化

无变化

创建集合并获取迭代器

迭代器记录 modCount

开始遍历

调用 next 方法

检查 modCount 是否变化?

抛出
ConcurrentModificationException

正常返回元素

还有下一个?

遍历结束

其他线程/本线程
直接修改集合

modCount++

3. 源码解析:fast-fail 机制的秘密

3.1 modCount 是什么?

ArrayList 继承自 AbstractList,其中有一个关键变量:

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
    // 记录集合结构性修改的次数
    protected transient int modCount = 0;
}

结构性修改指的是改变集合大小的操作,包括:

  • add() - 添加元素
  • remove() - 删除元素
  • clear() - 清空集合
  • addAll() - 批量添加
  • retainAll() - 保留指定元素

非结构性修改(不改变大小)不会影响 modCount:

  • set() - 替换元素

3.2 迭代器的实现

// ArrayList 的内部迭代器类
private class Itr implements Iterator<E> {
    int cursor;       // 下一个元素的下标
    int lastRet = -1; // 最后返回的元素下标
    int expectedModCount = modCount;  // 🔑 关键:期望的修改次数
    
    public E next() {
        checkForComodification();  // 每次 next 都会检查
        int i = cursor;
        // ... 获取元素
        return (E) elementData[lastRet = i];
    }
    
    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();  // 也会检查
        
        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;  // 修正期望值
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
    
    // 核心检查方法
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

3.3 异常触发流程图解

ArrayList 迭代器 业务代码 ArrayList 迭代器 业务代码 modCount = 0 获取迭代器 expectedModCount = modCount (0) next() 第1次 check: modCount(0) == expected(0) ✅ 返回 A list.remove("B") modCount++ (变为 1) next() 第2次 check: modCount(1) != expected(0) ❌ ConcurrentModificationException

4. 常见触发场景及解决方案

4.1 场景一:遍历时直接删除

// ❌ 错误写法
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String s : list) {
    if ("B".equals(s)) {
        list.remove(s);  // 抛出异常
    }
}

✅ 解决方案一:使用迭代器的 remove 方法

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if ("B".equals(s)) {
        it.remove();  // ✅ 正确:迭代器自己的 remove 方法
    }
}
System.out.println(list);  // [A, C]

✅ 解决方案二:倒序遍历

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (int i = list.size() - 1; i >= 0; i--) {
    if ("B".equals(list.get(i))) {
        list.remove(i);  // ✅ 倒序删除不影响遍历
    }
}

✅ 解决方案三:使用 removeIf(Java 8+)

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
list.removeIf(s -> "B".equals(s));  // ✅ 最简洁

4.2 场景二:增强 for 循环的本质

增强 for 循环只是语法糖,实际编译后还是迭代器:

// 源代码
for (String s : list) {
    if ("B".equals(s)) {
        list.remove(s);
    }
}

// 编译后(等价代码)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if ("B".equals(s)) {
        list.remove(s);  // ❌ 调用了 List 的方法,不是 Iterator 的
    }
}

4.3 场景三:多线程并发修改

// ❌ 多线程问题
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");

Thread t1 = new Thread(() -> {
    for (String s : list) {
        System.out.println(s);
        try { Thread.sleep(10); } catch (InterruptedException e) {}
    }
});

Thread t2 = new Thread(() -> {
    try { Thread.sleep(5); } catch (InterruptedException e) {}
    list.add("C");  // 另一个线程修改
});

t1.start();
t2.start();

✅ 解决方案:使用并发容器

// 方案1:CopyOnWriteArrayList(读多写少)
List<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
// 多线程安全,迭代器是快照

// 方案2:使用同步包装
List<String> list = Collections.synchronizedList(new ArrayList<>());
// 但迭代时仍需手动同步
synchronized (list) {
    Iterator<String> it = list.iterator();
    while (it.hasNext()) {
        it.next();
    }
}

4.4 场景四:subList 的坑

// ❌ subList 与原 list 共用同一个 modCount
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
List<String> sub = list.subList(0, 2);

list.add("E");  // 修改原 list,modCount 变化

for (String s : sub) {  // ❌ 抛出异常
    System.out.println(s);
}

✅ 解决方案

// 方案1:操作 subList 时不要操作原 list
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
List<String> sub = list.subList(0, 2);
sub.add("X");  // 操作 subList 是安全的

// 方案2:创建新的独立列表
List<String> sub = new ArrayList<>(list.subList(0, 2));

4.5 场景五:forEach 中的删除

// ❌ Java 8 forEach 同样会抛异常
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
list.forEach(s -> {
    if ("B".equals(s)) {
        list.remove(s);  // ❌ 抛出异常
    }
});

✅ 正确写法

// 使用 removeIf(推荐)
list.removeIf(s -> "B".equals(s));

// 或者先收集要删除的元素
List<String> toRemove = new ArrayList<>();
list.forEach(s -> {
    if ("B".equals(s)) {
        toRemove.add(s);
    }
});
list.removeAll(toRemove);

5. 不同集合的行为差异

集合类型 fast-fail 产生异常的场景 说明
ArrayList 遍历时结构性修改 最常用,最易触发
LinkedList 遍历时结构性修改 同 ArrayList
Vector 遍历时结构性修改 古老类,方法同步
HashSet 遍历时结构性修改 无序集合
HashMap 遍历时结构性修改 Key-Value 结构
CopyOnWriteArrayList 不产生 迭代器是快照
ConcurrentHashMap 不产生 弱一致性迭代器

6. Java 8+ 的最佳实践

6.1 使用 removeIf

// 删除所有空字符串
list.removeIf(String::isEmpty);

// 删除满足条件的元素
list.removeIf(s -> s.length() < 3);

6.2 使用 Stream

// 过滤后收集到新列表
List<String> filtered = list.stream()
    .filter(s -> !"B".equals(s))
    .collect(Collectors.toList());

// 或者修改原列表
list = list.stream()
    .filter(s -> !"B".equals(s))
    .collect(Collectors.toList());

6.3 使用 List.removeIf 源码

// ArrayList 的 removeIf 实现(Java 8)
default boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    boolean removed = false;
    final Iterator<E> each = iterator();
    while (each.hasNext()) {
        if (filter.test(each.next())) {
            each.remove();  // 内部使用了迭代器的 remove
            removed = true;
        }
    }
    return removed;
}

7. 特殊情况的处理

7.1 多线程环境下的安全删除

// 使用并发容器的 removeIf(线程安全)
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.addAll(Arrays.asList("A", "B", "C", "D"));

// CopyOnWriteArrayList 的 removeIf 是线程安全的
list.removeIf(s -> "B".equals(s));

7.2 边遍历边添加元素

// 需求:遍历时在特定位置后添加新元素

// 方案1:使用普通的 for 循环
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (int i = 0; i < list.size(); i++) {
    if ("B".equals(list.get(i))) {
        list.add(i + 1, "B2");  // 添加后 size 变化,循环继续
        i++;  // 跳过新加的元素
    }
}

// 方案2:创建新列表
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
List<String> newList = new ArrayList<>();
for (String s : list) {
    newList.add(s);
    if ("B".equals(s)) {
        newList.add("B2");
    }
}
list = newList;

8. 面试高频追问

8.1 fast-fail 和 safe-fail 的区别?

特性 fast-fail safe-fail
检测机制 检测 modCount 变化 不检测,基于快照
异常行为 立即抛出异常 不抛异常,遍历快照
代表集合 ArrayList, HashMap CopyOnWriteArrayList, ConcurrentHashMap
内存开销 正常 较高(快照)
一致性 强一致性 弱一致性

8.2 modCount 是 volatile 的吗?

不是。modCount 没有 volatile 修饰,这意味着在多线程环境下,一个线程对 modCount 的修改不一定立即对其他线程可见。但 fast-fail 机制的目的是尽早发现问题,不要求绝对准确。

8.3 为什么迭代器的 remove 不会抛异常?

因为迭代器的 remove 方法在删除后会同步更新 expectedModCount

public void remove() {
    // ...
    ArrayList.this.remove(lastRet);
    expectedModCount = modCount;  // 重新同步
    // ...
}

8.4 单线程下是否一定不会出现?

不一定!即使在单线程中,使用 subList 或通过其他方式修改集合,也可能触发:

List<String> list = new ArrayList<>();
list.add("A");
List<String> sub = list.subList(0, 1);
list.add("B");    // 修改原集合
sub.iterator().next();  // ❌ 抛出异常

9. 总结对比表

场景 错误做法 正确做法
遍历删除 for (item: list) { list.remove(item); } iterator.remove()removeIf()
遍历添加 for (item: list) { list.add(newItem); } 使用普通 for + 下标,或创建新列表
多线程遍历 多线程直接操作 ArrayList 使用 CopyOnWriteArrayList
subList 使用 修改原 list 后遍历 subList 操作 subList 时不要动原 list
forEach 删除 list.forEach(item -> list.remove(item)) list.removeIf()

一句话总结:

ConcurrentModificationException 是 Java 集合框架的 fast-fail 机制,用于检测迭代过程中的并发修改。其本质是迭代器创建时的 expectedModCount 与集合实际的 modCount 不一致。最佳解决方案是使用迭代器的 remove 方法、removeIf 表达式,或改用并发容器。


📌 记住:增强 for 循环是语法糖,底层还是迭代器;使用迭代器自己的修改方法才是安全的。

如果觉得本文对你有帮助,欢迎点赞、收藏、转发~

在这里插入图片描述


🌺The End🌺点点关注,收藏不迷路🌺
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐