java.util.ConcurrentModificationException异常出现的原因及解决方法
写在前面:大家好!我是
晴空๓
。如果博客中有不足或者的错误的地方欢迎在评论区或者私信我指正,感谢大家的不吝赐教。我的唯一博客更新地址是:https://ac-fun.blog.csdn.net/。非常感谢大家的支持。一起加油,冲鸭!
用知识改变命运,用知识成就未来!加油 (ง •̀o•́)ง (ง •̀o•́)ง
文章目录
- 前言
- for-each循环具体逻辑
- 快速失败迭代器
- 抛出该异常的情况
- 解决方案
- 使用迭代器的remove()方法
- 普通for循环遍历
- 避免在迭代过程中直接修改集合
前言
今天在写代码的时候碰到了异常 java.util.ConcurrentModificationException 出现这个异常的情况是使用增强for循环遍历集合,在遍历集合的时候修改集合(删除集合中的某个元素)。类似下面的代码:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ListTest {
public static void main(String[] args) {
List<String> nameList = new ArrayList<>();
nameList.add("Ye Wenjie");
nameList.add("Luo Ji");
nameList.add("Cheng Xin");
nameList.add("Wall-breaker");
nameList.add("Thomas Wade");
nameList.add("Wang Miao");
for (String name : nameList) {
// 判断是否符合剔除条件
if ("Wall-breaker".equals(name)) {
nameList.remove(name);
}
}
System.out.println(nameList);
}
}
执行代码之后抛出异常:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at ListTest.main(ListTest.java:15)
通过查看异常的堆栈跟踪信息及相关代码可以发现是因为使用 增强for循环 遍历列表时会调用 checkForComodification() 方法检查当前列表是否有并发修改,如果有则会抛出 java.util.ConcurrentModificationException 异常。
for-each循环具体逻辑
当遍历列表时调用了 ArrayList 类中的内部迭代器类 Itr,该类实现了 Iterator<E>
接口。具体是实现逻辑是什么呢?for-each循环 是如何调用该类中的方法的呢?我们可以通过 javap 命令反编译 .class 文件后可以看到如下信息(为了方便查看去除了不重要的信息):
Compiled from "ListTest.java"
public class ListTest {
public ListTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9~60: 字符串创建及初始化操作......
61: pop
62: aload_1
63: invokeinterface #11, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
68: astore_2
69: aload_2
70: invokeinterface #12, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
75: ifeq 108
78: aload_2
79: invokeinterface #13, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
84: checkcast #14 // class java/lang/String
87: astore_3
108: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
111: aload_1
112: invokevirtual #18 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
115: return
}
通过反编译代码的 63~84 可以发现首先调用了 List 接口的 iterator() 方法。这个方法返回一个迭代器对象,用于遍历列表。然后调用迭代器对象的 hasNext() 方法,检查是否还有下一个元素。如果有下一个元素则调用迭代器对象的 next() 方法,获取下一个元素;通过该逻辑实现循环遍历集合。可以发现该逻辑与我们使用 迭代器 进行循环遍历的逻辑是一样的,等价于下面这段利用迭代器遍历的代码:
// 使用迭代器遍历列表
Iterator<String> iterator = nameList.iterator();
while (iterator.hasNext()) {
String name = iterator.next();
// 判断是否符合剔除条件
if ("Wall-breaker".equals(name)) {
iterator.remove(); // 使用迭代器的remove方法移除元素
}
}
通过查看源码可以发现在调用迭代器对象的 next() 方法时调用的checkForComodification() 方法进行检查,具体代码如下:
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
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();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
checkForComodification() 方法会判断 modCount 是否与 expectedModCount 相等。modCount 是指 List 从 new 开始被修改的次数,当调用 remove()、add()、addAll() 等方法时该值会增加;expectedModCount 是指 Iterator 期望这个 list 被修改的次数是多少次。该值是在 Iterator 初始化的时候将 modCount 的值赋给了expectedModCount。
所以当我们在调用 增强for循环 的同时在循环内部调用 remove() 方法会导致这两个值不相等因此会抛出 ConcurrentModificationException() 异常。for-each循环实际上使用的是 快速失败迭代器,它不允许在迭代过程中修改集合。
快速失败迭代器
快速失败(fail-fast)行为在 Java 集合框架中是一种常见的行为模式,它具有以下好处:
- 检测并发修改:快速失败行为有助于检测在迭代过程中对集合的非预期结构性修改。这通常是由于集合被多个线程并发修改,或者在迭代过程中修改了集合,导致的并发修改异常。
- 提高安全性:通过抛出ConcurrentModificationException异常,快速失败行为可以防止程序在未检测到错误的情况下继续执行,从而避免可能的数据不一致性和不可预测的行为。
- 避免非确定性行为:如果没有快速失败机制,迭代器可能会在检测到并发修改时继续执行,导致不确定的结果。快速失败机制确保了一旦检测到修改,立即停止迭代,避免了非确定性行为。
- 调试和错误定位:当抛出ConcurrentModificationException异常时,开发者可以更容易地定位和理解代码中的错误,因为异常提供了一个明确的信号,表明代码中存在并发修改的问题。
- 保护数据完整性:快速失败机制可以防止在数据结构被修改时继续执行可能破坏数据完整性的操作。
- 代码清晰和维护性:使用快速失败迭代器的代码通常更易于理解和维护,因为它们明确地表明了不允许在迭代过程中修改集合。
- 避免隐藏的错误:如果没有快速失败机制,迭代器可能会在不抛出异常的情况下继续执行,这可能会导致更难以发现的错误,比如覆盖数据或者丢失更新。
抛出该异常的情况
通过搜索该异常可以发现 Java 中抛出该异常的情况通常包括以下几种:
-
在迭代过程中直接修改集合:在使用 for-each循环 或 迭代器 遍历集合时,如果直接通过集合对象调用 add()、remove() 等方法修改集合,可能会抛出此异常。
-
多线程并发修改:当一个线程在迭代集合的同时,另一个线程修改了集合的结构(如添加或删除元素)。
-
集合被多个线程同时修改,没有适当的同步机制:在多线程环境中,如果没有适当的同步控制,不同的线程可能会同时修改同一个集合,导致异常。
解决方案
通过以上描述我们可以发现该异常通常出现在集合在迭代过程中被直接修改或在多线程并发时同一个集合被不同的线程同时修改。解决方式主要有以下几种:
使用迭代器的remove()方法
在迭代过程中,如果需要删除元素,应该使用迭代器的 remove() 方法,而不是直接通过集合对象调用 remove()。通过以上描述我们可以了解到增强for循环实际上使用的是迭代器,我们如果想在遍历的同时修改元素列表可以将遍历方式改为迭代器遍历,使用迭代器的 remove() 方法。这是因为在调用迭代器的 remove() 方法后紧接着对 expectedModCount 进行了同步。移除源码如下:
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();
}
}
// ArrayList.this.remove(lastRet);源码
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
因为迭代器在调用移除方法时进行了同步,所以就不会因 expectedModCount != modCount 抛出 java.util.ConcurrentModificationException 异常。
普通for循环遍历
可以使用普通 for 循环遍历集合并在循环中删除元素**(不推荐该方式)**。使用该方法要注意手动调整索引,因为不手动调整会出现跳过元素的情况。如果使用正序遍历可以只在不删除元素时才增加索引,代码如下:
for (int i = 0; i < nameList.size(); ) {
if ("Cheng Xin".equals(nameList.get(i))) {
nameList.remove(i);
} else {
i++; // 只有当不删除当前元素时,索引才增加
}
}
也可以直接使用逆序循环,这样就不用手动调整索引,因为删除之后不会导致列表元素前移,代码如下:
for (int i = nameList.size() - 1; i >= 0; i--) {
if ("Cheng Xin".equals(nameList.get(i))) {
nameList.remove(i);
}
}
避免在迭代过程中直接修改集合
其实最好是避免在迭代过程中直接修改集合
。我们完全可以通过更好的设计避免在迭代的过程中修改集合。在迭代前或迭代后修改集合,或者收集需要修改的元素,迭代完成后统一处理更安全。在迭代过程中修改集合很容易出现不易发现的业务逻辑错误(例如忘记手动管理索引导致跳过元素)或者不可预测的行为,一旦出现比较难发现。因此我也修改了设计避免了在循环中修改集合。