Java Concurrent Modification Exception 全解析
简介
在 Java 编程中,ConcurrentModificationException
是一个常见且令人头疼的异常。当一个线程在遍历集合(如 ArrayList
、HashMap
等)的同时,另一个线程对该集合进行了结构上的修改(例如添加或删除元素),就可能抛出这个异常。理解这个异常的原理、出现场景以及如何避免它,对于编写健壮的多线程 Java 程序至关重要。
目录
- 基础概念
- 使用方法(其实不存在真正意义的使用,而是如何处理)
- 常见实践(出现场景)
- 最佳实践(如何避免)
- 小结
- 参考资料
基础概念
ConcurrentModificationException
是一个运行时异常(RuntimeException
的子类)。它主要在集合遍历过程中,检测到集合结构发生了意外的改变时抛出。
在 Java 的集合框架中,很多类(如 ArrayList
、HashMap
)不是线程安全的。当多个线程同时访问和修改这些集合时,就可能导致数据不一致或出现 ConcurrentModificationException
。
例如,考虑以下简单的代码:
import java.util.ArrayList;
import java.util.List;
public class ConcurrentModificationExample {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 主线程遍历列表
for (Integer num : list) {
System.out.println(num);
// 尝试在遍历过程中删除元素
list.remove(num);
}
}
}
运行上述代码,会抛出 ConcurrentModificationException
。这是因为在使用增强的 for
循环(for-each
)遍历集合时,实际上是使用了集合的迭代器(Iterator
)。而在遍历过程中调用 list.remove(num)
方法修改了集合的结构,导致迭代器检测到了这种不一致,从而抛出异常。
使用方法(如何处理)
通常情况下,我们不会主动去“使用” ConcurrentModificationException
,而是要处理它以确保程序的稳定性。
捕获异常
一种简单的处理方式是捕获异常:
import java.util.ArrayList;
import java.util.List;
public class ConcurrentModificationHandling {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
try {
for (Integer num : list) {
System.out.println(num);
list.remove(num);
}
} catch (ConcurrentModificationException e) {
System.out.println("捕获到 ConcurrentModificationException: " + e.getMessage());
}
}
}
这种方式虽然能防止程序因异常而崩溃,但并不能真正解决问题,只是在异常发生时进行了某种处理。
使用正确的迭代方式
更好的方法是使用迭代器的 remove
方法来删除元素,这样可以避免异常:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ProperIteration {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer num = iterator.next();
System.out.println(num);
iterator.remove();
}
}
}
在上述代码中,使用 Iterator
的 remove
方法,它会正确地更新集合的内部状态,从而避免 ConcurrentModificationException
。
常见实践(出现场景)
多线程环境下的集合操作
在多线程程序中,多个线程同时访问和修改一个非线程安全的集合时,很容易出现 ConcurrentModificationException
。
import java.util.ArrayList;
import java.util.List;
public class MultiThreadConcurrentModification {
private static List<Integer> sharedList = new ArrayList<>();
public static void main(String[] args) {
sharedList.add(1);
sharedList.add(2);
sharedList.add(3);
Thread thread1 = new Thread(() -> {
for (Integer num : sharedList) {
System.out.println("线程1: " + num);
}
});
Thread thread2 = new Thread(() -> {
sharedList.remove(2);
});
thread1.start();
thread2.start();
}
}
在上述代码中,thread1
遍历 sharedList
,而 thread2
同时尝试删除 sharedList
中的元素,这很可能导致 ConcurrentModificationException
。
递归遍历与修改
在递归方法中,如果在遍历集合的过程中进行结构修改,也可能触发此异常。
import java.util.ArrayList;
import java.util.List;
public class RecursiveConcurrentModification {
private static List<Integer> list = new ArrayList<>();
static {
list.add(1);
list.add(2);
list.add(3);
}
public static void recursiveTraversal(int index) {
if (index >= list.size()) {
return;
}
Integer num = list.get(index);
System.out.println(num);
list.remove(num);
recursiveTraversal(index);
}
public static void main(String[] args) {
recursiveTraversal(0);
}
}
上述递归方法在遍历列表的同时尝试删除元素,会引发 ConcurrentModificationException
。
最佳实践(如何避免)
使用线程安全的集合
Java 提供了一些线程安全的集合类,如 CopyOnWriteArrayList
和 ConcurrentHashMap
。
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class ThreadSafeCollectionExample {
public static void main(String[] args) {
List<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Thread thread1 = new Thread(() -> {
for (Integer num : list) {
System.out.println("线程1: " + num);
}
});
Thread thread2 = new Thread(() -> {
list.remove(2);
});
thread1.start();
thread2.start();
}
}
CopyOnWriteArrayList
在进行写操作(如添加或删除元素)时,会创建一个新的数组,读操作则在旧数组上进行,从而避免了并发修改问题。
使用同步机制
可以使用 synchronized
关键字或 ReentrantLock
来同步对集合的访问。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
public class SynchronizedCollectionExample {
private static List<Integer> sharedList = new ArrayList<>();
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
sharedList.add(1);
sharedList.add(2);
sharedList.add(3);
Thread thread1 = new Thread(() -> {
lock.lock();
try {
for (Integer num : sharedList) {
System.out.println("线程1: " + num);
}
} finally {
lock.unlock();
}
});
Thread thread2 = new Thread(() -> {
lock.lock();
try {
sharedList.remove(2);
} finally {
lock.unlock();
}
});
thread1.start();
thread2.start();
}
}
在上述代码中,通过 ReentrantLock
确保在同一时间只有一个线程可以访问和修改 sharedList
,从而避免了 ConcurrentModificationException
。
小结
ConcurrentModificationException
是 Java 多线程编程中常见的异常,主要由于在集合遍历过程中发生了意外的结构修改导致。为了避免这个异常,我们需要了解集合的线程安全性,正确使用迭代器,并在多线程环境中采取适当的同步机制或使用线程安全的集合类。通过遵循这些最佳实践,可以编写出更健壮、可靠的多线程 Java 程序。
参考资料
- Oracle Java 文档
- 《Effective Java》 - Joshua Bloch
- 《Java Concurrency in Practice》 - Brian Goetz 等