作为一名Java开发者,你是否曾在运行代码时突然遇到一个名为 ConcurrentModificationException 的异常?通常,这发生在我们正在遍历一个集合(例如 List 或 Map)的同时,试图修改它的时候。这种异常不仅让初学者感到困惑,即便是经验丰富的开发者,如果在复杂的业务逻辑中忽视了并发修改的细节,也难免会掉进这个陷阱。
在本文中,我们将深入探讨 ConcurrentModificationException 的本质。我们将不再仅仅停留在“报错了”的层面,而是通过分析Java集合框架的源码机制——特别是 Fail-Fast(快速失败) 机制,来理解它为什么会发生。更重要的是,我们将一起探索多种行之有效的解决方案,无论是处理单线程环境下的遍历删除,还是应对多线程并发场景,你都将从这篇文章中找到最佳实践。
什么是 ConcurrentModificationException?
在Java的异常体系中,INLINECODE9d26794b 位于 INLINECODEcfa460ca 包中。它继承自 RuntimeException,这意味着它是一个非受检异常。从定义上讲,当检测到对象的并发修改不被允许时,方法抛出此异常。
关于异常的基本分类,我们这里简单回顾一下,这有助于我们理解后续的处理策略:
- 受检异常:编译器强制要求处理的异常,例如 INLINECODEacbb98dd 或 INLINECODEad5b86f7。如果代码可能抛出这类异常,我们必须使用 INLINECODE2c69843b 捕获或者在方法签名上声明 INLINECODEf9d5a2e2。
- 非受检异常:包括 INLINECODEc1749f16 及其子类。编译器不强制要求处理。INLINECODE05758a85 就属于这一类,同时也包括著名的 INLINECODEc2cebae1 和 INLINECODEe82bb7de。
> 注意:虽然名字里带有“Concurrent(并发)”,但这个异常并不总是发生在多线程环境中。在单线程代码中,如果你使用不当的方式遍历并修改集合,同样会触发它。
核心原理:Fail-Fast 机制
要理解这个异常,我们必须深入了解Java集合框架的工作原理。许多集合类(如 INLINECODEfc2b9e40, INLINECODE06e5935b 等)并不是线程安全的。为了在迭代过程中尽早发现错误,Java引入了 Fail-Fast 机制。
这个机制的核心在于一个名为 modCount 的内部变量。
- INLINECODEa15ff3ad:记录了集合结构被修改的次数。每当添加(INLINECODE224ecdb4)或删除(INLINECODEfea0b2fa)一个元素导致集合结构发生变化时,INLINECODEcca46b95 都会自动加 1。
- 迭代器的预期值:当我们通过集合调用 INLINECODEaec245af 方法获取一个迭代器时,迭代器会记录当前集合的 INLINECODE0245e918 值,并将其存储为
expectedModCount。
触发条件:在迭代过程中,每次调用 INLINECODEe8d03b7a 方法时,迭代器都会检查集合当前的 INLINECODE2b342967 是否与自己的 INLINECODEe5af02c3 相等。如果不相等,就意味着集合的结构在迭代期间被其他人(或本段代码的其他部分)篡改了,迭代器会立即抛出 INLINECODEd39063d0,而不是继续执行可能导致不可预测结果的代码。
场景一:单线程下的“陷阱”
让我们通过一个经典的错误案例来看看这是如何发生的。这也是新手最容易遇到的情况:在遍历 List 时,试图根据条件删除元素。
#### 错误示例代码
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class RemoveWhileIteratingDemo {
public static void main(String[] args) {
// 1. 初始化一个列表
List products = new ArrayList();
products.add("iPhone 15");
products.add("MacBook Pro");
products.add("iPad Air");
products.add("Apple Watch");
products.add("AirPods");
System.out.println("原始列表: " + products);
// 2. 尝试在遍历时删除 "iPad Air"
Iterator iterator = products.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
// 危险操作:直接使用 List 的 remove 方法
if (item.equals("iPad Air")) {
products.remove(item); // 这里会抛出 ConcurrentModificationException
}
}
System.out.println("处理后的列表: " + products);
}
}
运行结果:
程序会在 INLINECODE2d906aba 这一行抛出异常,紧接着在 INLINECODEcfb8ce58 处终止。
原因分析:
当我们调用 INLINECODEace74eb8 时,ArrayList 中的 INLINECODE34dc3f5f 增加了 1。但是,迭代器 INLINECODE210721d7 中的 INLINECODE607c739f 还是旧的值。紧接着下一次循环调用 iterator.next() 进行检查时,发现数值不匹配,从而触发 Fail-Fast 机制。
#### 解决方案 1:使用迭代器的 remove() 方法
这是解决此类问题最标准、最推荐的方式。迭代器自身提供了 INLINECODE1df81a2b 方法,该方法会在删除元素后同步更新 INLINECODEecaa1989,从而保证一致性。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class CorrectRemoveDemo {
public static void main(String[] args) {
List products = new ArrayList();
products.add("iPhone 15");
products.add("MacBook Pro");
products.add("iPad Air");
products.add("Apple Watch");
System.out.println("原始列表: " + products);
Iterator iterator = products.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
// 正确操作:使用迭代器的 remove 方法
if (item.equals("iPad Air")) {
iterator.remove(); // 安全删除
}
}
System.out.println("处理后的列表: " + products);
}
}
#### 解决方案 2:使用 Java 8+ 的 removeIf 方法
如果你使用的是 Java 8 或更高版本,代码可以变得极其简洁。INLINECODEe9464ae1 接口新增了 INLINECODE56570b9e 方法,它内部已经处理了并发修改的问题,底层实现正是利用了迭代器。
import java.util.ArrayList;
import java.util.List;
public class RemoveIfDemo {
public static void main(String[] args) {
List products = new ArrayList();
products.add("iPhone 15");
products.add("MacBook Pro");
products.add("iPad Air");
products.add("Apple Watch");
// 使用 Lambda 表达式,一行代码搞定
// 这种方式不仅易读,而且非常安全
products.removeIf(item -> item.equals("iPad Air"));
System.out.println("处理后的列表: " + products);
}
}
场景二:多线程并发环境
在多线程环境下,情况会变得更加复杂。假设一个线程正在遍历集合,而另一个线程同时在修改这个集合,由于没有任何同步措施,modCount 的冲突是必然发生的。
#### 并发冲突示例
import java.util.ArrayList;
import java.util.List;
public class MultiThreadDemo {
public static void main(String[] args) {
List numbers = new ArrayList();
for (int i = 0; i {
for (Integer num : numbers) {
System.out.println("读取: " + num);
try { Thread.sleep(10); } catch (InterruptedException e) {}
}
});
// 线程2:负责修改集合
Thread writerThread = new Thread(() -> {
try { Thread.sleep(50); } catch (InterruptedException e) {}
// 这里直接操作集合
numbers.remove(50);
});
readerThread.start();
writerThread.start();
}
}
运行结果:这段代码极大概率会抛出 ConcurrentModificationException,因为主线程(或 Reader 线程)在迭代时,Writer 线程改变了结构。
#### 解决方案 1:使用 CopyOnWriteArrayList
对于读多写少的并发场景,INLINECODE723264ef 包下的 INLINECODE78f342e3 是救星。正如其名,每当发生修改时,它会复制底层数组。因此,迭代器遍历的是创建时的数组快照,不会受到后续写入操作的影响。
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class SafeConcurrentDemo {
public static void main(String[] args) {
// 使用线程安全的 CopyOnWriteArrayList
List numbers = new CopyOnWriteArrayList();
for (int i = 0; i {
for (Integer num : numbers) {
System.out.println("读取: " + num);
try { Thread.sleep(10); } catch (InterruptedException e) {}
}
});
Thread writerThread = new Thread(() -> {
try { Thread.sleep(50); } catch (InterruptedException e) {}
// 安全修改
numbers.remove(50);
System.out.println("已移除元素 50");
});
readerThread.start();
writerThread.start();
}
}
#### 解决方案 2:使用同步块
如果你不想使用 INLINECODEed17c7dd(因为每次修改都要复制数组,开销较大),可以使用 INLINECODE5ab1ab35 关键字。但请注意,你必须在遍历时对集合对象加锁,并且修改线程也需要持有同一个锁,这会增加代码的复杂度并可能降低性能。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SynchronizedListDemo {
public static void main(String[] args) {
List numbers = new ArrayList();
// 包装成同步列表
List syncList = Collections.synchronizedList(numbers);
for (int i = 0; i < 100; i++) {
syncList.add(i);
}
// 遍历时必须手动加锁
synchronized (syncList) {
for (Integer num : syncList) {
System.out.println("安全读取: " + num);
}
}
}
}
场景三:在增强 For 循环中修改
你可能会问,我不用显式的 INLINECODE92e0cf91,我用增强型 INLINECODEd9b1c15c 循环(也就是“for-each”循环)总行了吧?
答案是:不行。
Java 的增强 for 循环在编译后,实际上会被转换为使用 INLINECODE8e501c3a 的方式。因此,你在 for-each 循环体内直接调用集合的 INLINECODEbf96463c 方法,依然会报错。
#### 错误代码
List list = new ArrayList();
list.add("A");
list.add("B");
// 编译后的代码本质上使用了 Iterator
for (String s : list) {
if (s.equals("A")) {
list.remove(s); // 抛出 ConcurrentModificationException
}
}
解决办法:请参考前文提到的使用显式 INLINECODE499bae5c 调用 INLINECODE45141df3,或者使用 removeIf。
最佳实践总结与性能建议
为了让你在日后的开发中能够游刃有余,我们总结了以下几条建议:
- 首选 INLINECODE8b738ff9:如果你使用的是 Java 8+,且逻辑相对简单,优先使用 INLINECODE32c7f4c5。这是最现代、最简洁、最不易出错的写法。
- 显式迭代器:在旧版本 Java 或逻辑复杂需要手动控制时,务必使用 INLINECODE7a9554df 及其 INLINECODEf7d3c5cd 方法。不要在遍历时直接操作集合本身。
- 并发场景的选择:
* 如果是多线程写多读少:使用 INLINECODEab2ed6e9 配合手动锁,或者考虑 INLINECODE02b58631 等并发容器。
* 如果是多线程读多写少:CopyOnWriteArrayList 是极好的选择,因为遍历速度极快(不需要加锁),且绝对安全。
- 注意 modCount 检查时机:异常通常是在调用 INLINECODE0747e0ca 方法时触发的,而不是在 INLINECODEe4509d7b 时。有时候你执行了删除代码,没报错,但下一次循环获取下一个元素时立刻崩溃,这往往会让人误以为是删除下一行元素出了问题,请务必警惕。
结语
INLINECODE158d682e 并不是Java故意在为难我们,相反,它是Fail-Fast机制的一种善意提醒,防止程序在不一致的状态下继续运行从而产生更严重的数据错误。通过理解 INLINECODEb2e1484c 的工作原理,并熟练运用 INLINECODEf9f7dfeb、INLINECODE40e61b07 以及并发工具类,我们完全可以轻松化解这一异常。
希望这篇文章能帮助你彻底解决这个困扰已久的问题。下次当你遇到这个异常时,你不仅知道怎么修,还知道为什么会发生,以及如何写出更健壮的代码。
祝你编码愉快!