目录
一、ConcurrentModificationException 的基本概念
二、ConcurrentModificationException 的触发场景
三、ConcurrentModificationException 的底层原理
四、解决 ConcurrentModificationException 的方法
方法 1:使用 Iterator 的 remove() 方法
前言
在 Java 的日常开发中,ConcurrentModificationException
(并发修改异常)是一个常见的异常,尤其是在操作集合(如 List
、Map
等)时。如果对集合进行迭代的同时又修改了集合,就有可能抛出这个异常。本文将对 ConcurrentModificationException
进行深入剖析,了解其产生的原因,以及解决该异常的多种方式。
一、ConcurrentModificationException
的基本概念
1. 定义
ConcurrentModificationException
是 Java 中的运行时异常,位于包 java.util
下。它表示当一个线程对集合进行迭代的过程中,同时检测到另一个线程或同一线程对该集合进行了结构性修改时抛出的异常。
2. 什么是结构性修改?
结构性修改是指改变集合的结构,例如:
- 添加元素
- 删除元素
- 清空集合
非结构性修改(如通过 set()
方法替换列表中的某个值)通常不会触发此异常,因为它不会改变集合的结构。
二、ConcurrentModificationException
的触发场景
1. 单线程环境中的触发
即使在单线程环境中,如果在迭代集合时直接对集合进行增删操作,也会抛出该异常。例如:
import java.util.ArrayList;
import java.util.List;
public class SingleThreadExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (String item : list) {
if ("A".equals(item)) {
list.remove(item); // 在迭代时修改集合
}
}
}
}
//输出
Exception in thread "main" java.util.ConcurrentModificationException
原因:for-each
循环底层使用了 Iterator
。当通过 Iterator
遍历集合时,直接对集合进行修改会触发 modCount
不一致,从而抛出异常。
2. 多线程环境中的触发
在多线程环境下,一个线程正在遍历集合,而另一个线程对集合进行修改,也可能抛出该异常。例如:
import java.util.ArrayList;
import java.util.List;
public class MultiThreadExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
new Thread(() -> {
for (String item : list) {
System.out.println(item);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
try {
Thread.sleep(20);
list.add("D"); // 修改集合
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
输出:可能会抛出 ConcurrentModificationException
原因:两个线程同时操作集合,导致迭代器检测到集合的 modCount
被改变。
三、ConcurrentModificationException
的底层原理
在 Java 集合(如 ArrayList
、HashMap
)中,迭代器会维护一个 modCount
变量来跟踪集合的修改次数。当迭代器创建时,它会保存当前的 modCount
值。在迭代过程中,每次调用 next()
方法时,迭代器会检查集合当前的 modCount
是否与初始值一致。如果不一致,就说明集合被修改过,于是抛出 ConcurrentModificationException
。
modCount
的核心代码示例如下(以 ArrayList
为例):
public class ArrayList<E> extends AbstractList<E> {
protected transient int modCount = 0; // 修改计数
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
public E remove(int index) {
modCount++; // 每次结构性修改都会增加 modCount
E oldValue = elementData(index);
fastRemove(index);
return oldValue;
}
}
在 Iterator
中的检查逻辑:
public E next() {
if (modCount != expectedModCount) // 检查是否有不一致
throw new ConcurrentModificationException();
return (E) ArrayList.this.elementData(cursor++);
}
四、解决 ConcurrentModificationException
的方法
方法 1:使用 Iterator
的 remove()
方法
如果需要在迭代时修改集合,可以使用 Iterator
提供的 remove()
方法,而不是直接调用集合的 remove()
方法。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class IteratorRemoveExample {
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 item = iterator.next();
if ("A".equals(item)) {
iterator.remove(); // 正确的删除方式
}
}
System.out.println(list); // 输出:[B, C]
}
}
方法 2:使用 CopyOnWriteArrayList
在多线程环境中,使用 CopyOnWriteArrayList
替代普通的 ArrayList
。CopyOnWriteArrayList
是线程安全的,它在每次修改时都会创建集合的副本,从而避免 ConcurrentModificationException
。
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListExample {
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (String item : list) {
if ("A".equals(item)) {
list.remove(item); // 不会抛出 ConcurrentModificationException
}
}
System.out.println(list); // 输出:[B, C]
}
}
注意:CopyOnWriteArrayList
在修改性能上较差,适用于读多写少的场景。
方法 3:使用普通的 for
循环
普通的 for
循环可以直接操作集合,不会触发迭代器的 modCount
检查。
import java.util.ArrayList;
import java.util.List;
public class ForLoopExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (int i = 0; i < list.size(); i++) {
if ("A".equals(list.get(i))) {
list.remove(i); // 直接删除
i--; // 删除后需要调整索引
}
}
System.out.println(list); // 输出:[B, C]
}
}
方法 4:使用 Synchronized
同步块
在多线程环境中,可以使用同步块对集合进行显式加锁,从而避免其他线程同时修改集合。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SynchronizedExample {
public static void main(String[] args) {
List<String> list = Collections.synchronizedList(new ArrayList<>());
list.add("A");
list.add("B");
list.add("C");
synchronized (list) { // 显式加锁
for (String item : list) {
if ("A".equals(item)) {
list.remove(item);
}
}
}
System.out.println(list); // 输出:[B, C]
}
}
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SynchronizedExample {
public static void main(String[] args) {
List<String> list = Collections.synchronizedList(new ArrayList<>());
list.add("A");
list.add("B");
list.add("C");
synchronized (list) { // 显式加锁
for (String item : list) {
if ("A".equals(item)) {
list.remove(item);
}
}
}
System.out.println(list); // 输出:[B, C]
}
}
五、总结
ConcurrentModificationException
(并发修改异常) 的出现通常是因为在迭代集合时同时修改了集合。要避免该异常,应该根据具体场景选择合适的解决方案:
- 在单线程环境下,使用迭代器的
remove()
方法。 - 在多线程环境下,使用线程安全的集合如
CopyOnWriteArrayList
或对集合加锁。 - 对于简单场景,可以使用普通的
for
循环。
通过合理的方式处理集合的修改,不仅能避免异常,还能提升程序的稳定性和可维护性。希望本文能帮助您深入理解 ConcurrentModificationException
及其解决方案。如有任何疑问,欢迎交流!