JAVA 集合类并发修改异常:ConcurrentModificationException 根因详解
大家好,今天我们来深入探讨一个在Java集合类使用中非常常见,也经常让开发者头疼的异常:ConcurrentModificationException。 我们将从异常的定义,产生的原因,到如何有效地避免它,进行详细的分析。
1. ConcurrentModificationException 的定义与场景
ConcurrentModificationException 是一个运行时异常,它在 java.util 包中定义。 当检测到在迭代集合的过程中,集合的结构被修改了(增加、删除元素),并且这种修改不是通过迭代器自身的方法进行的,就会抛出这个异常。
简单的说,就是你在用迭代器遍历集合,同时又用集合自身的方法增删元素,就可能出现这个异常。
常见的场景包括:
- 单线程中使用迭代器遍历集合,同时使用集合自身的
add或remove方法修改集合。 - 多线程环境下,多个线程同时修改同一个集合。
2. ConcurrentModificationException 的根因:快速失败机制 (Fail-Fast)
ConcurrentModificationException 异常的产生,实际上是 Java 集合类设计中一种称为“快速失败”(Fail-Fast)机制的体现。 快速失败机制的核心思想是:在检测到并发修改时,立即抛出异常,而不是让程序在不一致的状态下继续运行,导致更难以追踪的错误。
2.1 fail-fast机制的实现
Java 集合类(例如 ArrayList, LinkedList, HashMap 等) 通常会维护一个 modCount 变量。 这个变量记录了集合被修改的次数(结构上的修改,例如增加或删除元素)。
当创建一个迭代器时,迭代器会记录下集合的当前 modCount 值 (通常存储在迭代器的 expectedModCount 变量中)。 在迭代过程中,每次调用迭代器的 next() 方法时,迭代器都会检查集合的 modCount 是否与 expectedModCount 相等。
- 如果相等,说明集合在迭代过程中没有被修改,迭代器继续正常工作。
- 如果不相等,说明集合在迭代过程中被修改了,迭代器会立即抛出
ConcurrentModificationException异常。
2.2 为什么需要快速失败机制?
如果没有快速失败机制,当多个线程或者在单线程中通过集合自身方法修改集合时,迭代器可能无法感知到这些修改。 这可能导致:
- 迭代器返回错误的结果。
- 程序进入死循环。
- 程序崩溃。
快速失败机制可以帮助开发者尽早地发现和修复并发修改的问题,避免程序在不一致的状态下运行,从而提高程序的可靠性。
3. 代码示例与分析
3.1 单线程下的 ConcurrentModificationException
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ConcurrentModificationExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println("当前元素:" + element);
if (element.equals("B")) {
list.remove("B"); // 导致 ConcurrentModificationException
}
}
}
}
分析:
这段代码会抛出 ConcurrentModificationException 异常。 原因是在迭代过程中,当迭代到元素 "B" 时, 使用 list.remove("B") 方法直接修改了集合的结构。 此时,迭代器的 expectedModCount 值与集合的 modCount 值不一致,导致迭代器抛出异常。
3.2 多线程下的 ConcurrentModificationException
import java.util.ArrayList;
import java.util.List;
public class ConcurrentModificationMultiThread {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Runnable iteratorTask = () -> {
for (String element : list) {
System.out.println(Thread.currentThread().getName() + ": " + element);
try {
Thread.sleep(10); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable modificationTask = () -> {
try {
Thread.sleep(5); // 稍微延迟一下
} catch (InterruptedException e) {
e.printStackTrace();
}
list.remove("B");
System.out.println(Thread.currentThread().getName() + ": Removed B");
};
Thread iteratorThread = new Thread(iteratorTask, "IteratorThread");
Thread modificationThread = new Thread(modificationTask, "ModificationThread");
iteratorThread.start();
modificationThread.start();
iteratorThread.join();
modificationThread.join();
System.out.println("程序结束");
}
}
分析:
这个例子中,一个线程负责迭代集合,另一个线程负责修改集合。 由于 ArrayList 不是线程安全的,在多线程环境下,同时修改集合会导致 modCount 变量出现竞争条件, 从而导致 ConcurrentModificationException 异常。 虽然不一定每次都出现,但这种并发修改的风险是存在的。
4. 避免 ConcurrentModificationException 的方法
4.1 使用迭代器自身的 remove() 方法
如果在迭代过程中需要删除元素,应该使用迭代器自身的 remove() 方法,而不是集合的 remove() 方法。 迭代器的 remove() 方法会在删除元素的同时,更新迭代器的 expectedModCount 值, 使其与集合的 modCount 值保持一致,从而避免抛出异常。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class CorrectIteratorRemove {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println("当前元素:" + element);
if (element.equals("B")) {
iterator.remove(); // 使用迭代器自身的 remove() 方法
}
}
System.out.println("修改后的集合:" + list);
}
}
4.2 使用线程安全的集合类
在多线程环境下,应该使用线程安全的集合类,例如 ConcurrentHashMap, CopyOnWriteArrayList, ConcurrentLinkedQueue 等。 这些集合类在设计时考虑了并发访问的情况,能够保证线程安全,避免 ConcurrentModificationException 异常。
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.List;
public class ThreadSafeList {
public static void main(String[] args) throws InterruptedException {
List<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Runnable iteratorTask = () -> {
for (String element : list) {
System.out.println(Thread.currentThread().getName() + ": " + element);
try {
Thread.sleep(10); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable modificationTask = () -> {
try {
Thread.sleep(5); // 稍微延迟一下
} catch (InterruptedException e) {
e.printStackTrace();
}
list.remove("B");
System.out.println(Thread.currentThread().getName() + ": Removed B");
};
Thread iteratorThread = new Thread(iteratorTask, "IteratorThread");
Thread modificationThread = new Thread(modificationTask, "ModificationThread");
iteratorThread.start();
modificationThread.start();
iteratorThread.join();
modificationThread.join();
System.out.println("程序结束, 修改后的集合:" + list);
}
}
4.3 使用 java.util.Collections.synchronizedList() 包装
可以使用 java.util.Collections.synchronizedList() 方法将普通的 ArrayList 或 LinkedList 包装成线程安全的集合。 但是要注意,虽然包装后的集合是线程安全的,但在迭代时仍然需要进行额外的同步,以避免 ConcurrentModificationException 异常。
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
public class SynchronizedListExample {
public static void main(String[] args) {
List<String> list = Collections.synchronizedList(new ArrayList<>());
list.add("A");
list.add("B");
list.add("C");
synchronized (list) { // 需要额外的同步
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println("当前元素:" + element);
if (element.equals("B")) {
iterator.remove(); // 使用迭代器自身的 remove() 方法
}
}
}
System.out.println("修改后的集合:" + list);
}
}
4.4 使用 Stream API 进行过滤和收集
Java 8 引入的 Stream API 提供了一种更简洁、更安全的方式来处理集合。 可以使用 filter() 方法过滤元素,然后使用 collect() 方法将过滤后的元素收集到一个新的集合中。 这种方式避免了直接修改原始集合,从而避免了 ConcurrentModificationException 异常。
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class StreamApiExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
List<String> filteredList = list.stream()
.filter(element -> !element.equals("B"))
.collect(Collectors.toList());
System.out.println("原始集合:" + list);
System.out.println("过滤后的集合:" + filteredList);
}
}
4.5 复制集合进行操作
可以先复制一份集合的副本,然后在副本上进行修改,最后再将修改后的副本赋值给原始集合。 这种方式避免了直接修改原始集合,从而避免了 ConcurrentModificationException 异常。 但需要注意复制集合的开销。
import java.util.ArrayList;
import java.util.List;
public class CopyListExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
List<String> copyList = new ArrayList<>(list); // 复制集合
for (int i = 0; i < copyList.size(); i++) {
String element = copyList.get(i);
if (element.equals("B")) {
copyList.remove(i);
break; // 删除后跳出循环,避免索引越界
}
}
list.clear(); // 清空原始集合
list.addAll(copyList); // 将修改后的副本赋值给原始集合
System.out.println("修改后的集合:" + list);
}
}
5. 不同集合类的线程安全性
为了更好地选择合适的集合类,下面列出一些常用集合类的线程安全性:
| 集合类 | 线程安全性 | 说明 |
|---|---|---|
ArrayList |
不安全 | 默认情况下,ArrayList 不是线程安全的。 在多线程环境下,需要进行额外的同步处理。 |
LinkedList |
不安全 | 默认情况下,LinkedList 不是线程安全的。 在多线程环境下,需要进行额外的同步处理。 |
HashMap |
不安全 | 默认情况下,HashMap 不是线程安全的。 在多线程环境下,需要进行额外的同步处理。 |
HashSet |
不安全 | 默认情况下,HashSet 不是线程安全的。 在多线程环境下,需要进行额外的同步处理。 |
TreeMap |
不安全 | 默认情况下,TreeMap 不是线程安全的。 在多线程环境下,需要进行额外的同步处理。 |
TreeSet |
不安全 | 默认情况下,TreeSet 不是线程安全的。 在多线程环境下,需要进行额外的同步处理。 |
Vector |
安全 | Vector 是线程安全的,但其线程安全是通过在每个方法上添加 synchronized 关键字来实现的,性能较低。 |
Hashtable |
安全 | Hashtable 是线程安全的,但其线程安全是通过在每个方法上添加 synchronized 关键字来实现的,性能较低。 |
ConcurrentHashMap |
安全 | ConcurrentHashMap 是线程安全的,它使用分段锁来实现更高的并发性能。 |
CopyOnWriteArrayList |
安全 | CopyOnWriteArrayList 是线程安全的,它通过复制整个数组来实现写操作的线程安全。 适用于读多写少的场景。 |
ConcurrentLinkedQueue |
安全 | ConcurrentLinkedQueue 是线程安全的,它使用 CAS (Compare and Swap) 操作来实现无锁并发。 |
6. 总结
ConcurrentModificationException 异常是 Java 集合类快速失败机制的体现,用于检测并发修改。 在单线程环境下,应该使用迭代器自身的 remove() 方法来删除元素。 在多线程环境下,应该使用线程安全的集合类,或者进行额外的同步处理。 使用 Stream API 或复制集合也可以避免这个异常。
7. 几个要点回顾
- 快速失败机制:
ConcurrentModificationException是快速失败机制的体现,用于尽早发现并发修改问题。 - 线程安全性: 选择合适的线程安全集合类是避免
ConcurrentModificationException的关键。 - 迭代器使用: 在迭代过程中修改集合结构时,必须使用迭代器自身的方法。
希望今天的分享能够帮助大家更好地理解 ConcurrentModificationException 异常,并在实际开发中避免它,编写出更健壮、更可靠的 Java 代码。 谢谢大家!