当前位置: 首页 > article >正文

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 是指 Listnew 开始被修改的次数,当调用 remove()、add()、addAll() 等方法时该值会增加;expectedModCount 是指 Iterator 期望这个 list 被修改的次数是多少次。该值是在 Iterator 初始化的时候将 modCount 的值赋给了expectedModCount

 所以当我们在调用 增强for循环 的同时在循环内部调用 remove() 方法会导致这两个值不相等因此会抛出 ConcurrentModificationException() 异常。for-each循环实际上使用的是 快速失败迭代器,它不允许在迭代过程中修改集合。

快速失败迭代器

 快速失败(fail-fast)行为在 Java 集合框架中是一种常见的行为模式,它具有以下好处:

  1. 检测并发修改:快速失败行为有助于检测在迭代过程中对集合的非预期结构性修改。这通常是由于集合被多个线程并发修改,或者在迭代过程中修改了集合,导致的并发修改异常。
  2. 提高安全性:通过抛出ConcurrentModificationException异常,快速失败行为可以防止程序在未检测到错误的情况下继续执行,从而避免可能的数据不一致性和不可预测的行为。
  3. 避免非确定性行为:如果没有快速失败机制,迭代器可能会在检测到并发修改时继续执行,导致不确定的结果。快速失败机制确保了一旦检测到修改,立即停止迭代,避免了非确定性行为。
  4. 调试和错误定位:当抛出ConcurrentModificationException异常时,开发者可以更容易地定位和理解代码中的错误,因为异常提供了一个明确的信号,表明代码中存在并发修改的问题。
  5. 保护数据完整性:快速失败机制可以防止在数据结构被修改时继续执行可能破坏数据完整性的操作。
  6. 代码清晰和维护性:使用快速失败迭代器的代码通常更易于理解和维护,因为它们明确地表明了不允许在迭代过程中修改集合。
  7. 避免隐藏的错误:如果没有快速失败机制,迭代器可能会在不抛出异常的情况下继续执行,这可能会导致更难以发现的错误,比如覆盖数据或者丢失更新。

抛出该异常的情况

 通过搜索该异常可以发现 Java 中抛出该异常的情况通常包括以下几种:

  1. 在迭代过程中直接修改集合:在使用 for-each循环迭代器 遍历集合时,如果直接通过集合对象调用 add()、remove() 等方法修改集合,可能会抛出此异常。

  2. 多线程并发修改:当一个线程在迭代集合的同时,另一个线程修改了集合的结构(如添加或删除元素)。

  3. 集合被多个线程同时修改,没有适当的同步机制:在多线程环境中,如果没有适当的同步控制,不同的线程可能会同时修改同一个集合,导致异常。

解决方案

 通过以上描述我们可以发现该异常通常出现在集合在迭代过程中被直接修改或在多线程并发时同一个集合被不同的线程同时修改。解决方式主要有以下几种:

使用迭代器的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);
    }
}

避免在迭代过程中直接修改集合

 其实最好是避免在迭代过程中直接修改集合。我们完全可以通过更好的设计避免在迭代的过程中修改集合。在迭代前或迭代后修改集合,或者收集需要修改的元素,迭代完成后统一处理更安全。在迭代过程中修改集合很容易出现不易发现的业务逻辑错误(例如忘记手动管理索引导致跳过元素)或者不可预测的行为,一旦出现比较难发现。因此我也修改了设计避免了在循环中修改集合。


http://www.kler.cn/a/448253.html

相关文章:

  • Linux 网络维护相关命令简介
  • linux 中文输入法设置的宏观思路 (****)
  • 全脐点曲面当且仅当平面或者球面的一部分
  • HTTP—03
  • web3跨链桥协议-Nomad
  • 大型系统中的 MySQL 部署与优化(一)
  • 【python实现烟花】
  • 解锁 SSM 与 Vue 在新锐台球厅管理系统设计与实现中的融合密码
  • 【大语言模型】ACL2024论文-29 答案即所需:通过回答问题实现指令跟随的文本嵌入
  • 【多维DP】【准NOI难度】力扣3251. 单调数组对的数目 II
  • 爬虫代码中如何处理异常?
  • 【面试 - 遇到的问题】Vue 里 router-view 使用 key + 关闭页面后重新打开页面-获取的数据赋值到旧组件问题(钩子执行顺序)
  • oracle使用imp命令导入dmp文件
  • 方正畅享全媒体新闻采编系统 reportCenter.do Sql注入漏洞复现(附脚本)
  • Dalsa线阵CCD相机使用开发手册
  • EasyPoi 使用$fe:模板语法生成Word动态行
  • sass的用法
  • 36. Three.js案例-创建带光照和阴影的球体与平面
  • 四、使用langchain搭建RAG:金融问答机器人--构建web应用,问答链,带记忆功能
  • 常用类晨考day15
  • 重撸设计模式--代理模式
  • Git使用教程-分支使用/合并分支提交
  • 抖音SEO短视频矩阵源码系统开发分享
  • 使用复数类在C#中轻松绘制曼德布洛集分形
  • LeetCode---428双周赛
  • 电子电器架构 ---证书认证需求及CANoe验证脚本