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.ConcurrentModificationExceptionat 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_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: new           #2                  // class java/util/ArrayList3: dup4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V7: astore_18: aload_19~60: 字符串创建及初始化操作......61: pop62: aload_163: invokeinterface #11,  1           // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;68: astore_269: aload_270: invokeinterface #12,  1           // InterfaceMethod java/util/Iterator.hasNext:()Z75: ifeq          10878: aload_279: invokeinterface #13,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;84: checkcast     #14                 // class java/lang/String87: astore_3108: getstatic     #17                 // Field java/lang/System.out:Ljava/io/PrintStream;111: aload_1112: invokevirtual #18                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V115: 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 returnint lastRet = -1; // index of last element returned; -1 if no suchint 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 trafficcursor = 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 workreturn 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.mzph.cn/news/890394.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构---------二叉树前序遍历中序遍历后序遍历

以下是用C语言实现二叉树的前序遍历、中序遍历和后序遍历的代码示例&#xff0c;包括递归和非递归&#xff08;借助栈实现&#xff09;两种方式&#xff1a; 1. 二叉树节点结构体定义 #include <stdio.h> #include <stdlib.h>// 二叉树节点结构体 typedef struct…

UE5仿漫威争锋灵蝶冲刺技能

这两天玩了一下漫威争锋Marvel Rivals&#xff0c;发现是UE5做的&#xff0c;对里面一些角色技能挺感兴趣的&#xff0c;想简单复刻一下技能功能&#xff0c;顺便复习一下学过的知识 首先把摄像机设置调整一下 CameraBoom里搜索lag 把摄像机延迟关掉 &#xff0c;这样摄像机就…

常用类晨考day15

1.基本数据类型以及对应包装类 Byte Short Integer Long Float Double Boolean Character 2.什么是自动拆箱和装箱&#xff0c;jdk版本有什么要求&#xff1f;代码举 例并标明 Integer a 100; // 装箱 int b a; // 拆箱 从JDK1.5才开始支持 3.NumberFormatException是什么异常…

etcd+京东hotkey探测使用

qhotKey链接 京东hotkey把热点数据默认缓存在了本地缓存caffeine中&#xff0c;也可以存到redis中&#xff0c;但是京东hotkey的SDK没有redis的实现方法&#xff0c;因此需要自己实现。 官方目录结构下&#xff1a;分别是client客户端&#xff08;要打包引入到自己的项目&…

如何实现层叠布局

文章目录 1 概念介绍2 使用方法3 示例代码我们在上一章回中介绍了GirdView Widget,本章回中将介绍Stack这种Widget,闲话休提,让我们一起Talk Flutter吧。 1 概念介绍 在Flutter中Stack主要用来叠加显示其它的Widget,类似我们日常生活中的楼层或者说PS中的图层,因此它也是一…

Java 上机实践11(组件及事件处理)

&#xff08;大家好&#xff0c;今天分享的是Java的相关知识&#xff0c;大家可以在评论区进行互动答疑哦~加油&#xff01;&#x1f495;&#xff09; 目录 Plug&#xff1a;程序实现 方法一&#xff08;记事本&#xff09; 方法二&#xff08;IDEA&#xff09; 实验一&am…

本地如何启动casdoor

1、下载代码 GitHub - casdoor/casdoor at v1.777.0 下载对应tag的代码&#xff0c;我这里选择的时v1.777.0版本 通过网盘分享的文件&#xff1a;casdoor-1.777.0.zip 链接: https://pan.baidu.com/s/1fPNqyJYeyfZnem_LtEc0hw 提取码: avpd 2、启动后端 1、使用goland编译…

CSDN外链失效3:

参考我之前的博客&#xff1a; 外链失效博客1&#xff1a;随想笔记1&#xff1a;CSDN写博客经常崩溃&#xff0c;遇到外链图片转存失败怎么办_csdn外链图片转存失败-CSDN博客 外链失效博客2&#xff1a;网络随想2&#xff1a;转语雀_md格式转语雀lake格式-CSDN博客 markdown…

Kubernates

kubernates是一个开源的&#xff0c;用于管理云平台中多个主机上的容器化的应用&#xff0c;Kubernetes的目标是让部署容器化的应用简单并且高效&#xff08;powerful&#xff09;,Kubernetes提供了应用部署&#xff0c;规划&#xff0c;更新&#xff0c;维护的一种机制。 架构…

Pycharm 更改字体大小

更改代码字体的大小 更改软件字体的大小

Ubuntu20.04解决docker安装后is the docker daemon running? 问题

Ubuntu20.04解决docker安装后is the docker daemon running? 问题 问题描述问题分析问题解决 问题描述 docker info后报错 ERROR: Cannot connect to the Docker daemon at unix:///root/.docker/desktop/docker.sock. Is the docker daemon running? errors pretty printi…

四、使用langchain搭建RAG:金融问答机器人--构建web应用,问答链,带记忆功能

经过前面3节完成金融问答机器人基本流程&#xff0c;这章将使用Gradio构建web应用&#xff0c;同时加入memory令提示模板带有记忆的&#xff0c;使用LCEL构建问答链。 加载向量数据库 from langchain.vectorstores import Chroma from langchain_huggingface import HuggingF…

深度学习之超分辨率算法——SRCNN

网络为基础卷积层 tensorflow 1.14 scipy 1.2.1 numpy 1.16 大概意思就是针对数据&#xff0c;我们先把图片按缩小因子照整数倍进行缩减为小图片&#xff0c;再针对小图片进行插值算法&#xff0c;获得还原后的低分辨率的图片作为标签。 main.py 配置文件 from model im…

基于海思soc的智能产品开发(mcu读保护的设置)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 对于市场上的产品&#xff0c;除了电路之外&#xff0c;软件保护也是非常重要的一个环节。要是自己辛辛苦苦写的软件&#xff0c;被竞争对手轻易地…

Zabbix6.0升级为6.4

为了体验一些新的功能&#xff0c;比如 Webhook 和问题抑制等&#xff0c;升级个小版本。 一、环境信息 1. 版本要求 一定要事先查看官方文档&#xff0c;确认组件要求的版本&#xff0c;否则版本过高或者过低都会出现问题。 2. 升级前后信息 环境升级前升级后操作系统CentOS…

GitLab的卸载与重装

目录 一、GitLab的卸载 二、 GitLab的安装与配置 1. 创建安装目录 2. 安装 3. 使用 3.1 初始化 3.2 创建空白项目 ​编辑 3.3 配置SSH 3.3.1 配置公钥 ​编辑 3.3.2 配置私钥 3.4 配置本地git库 一、GitLab的卸载 1. 停止gitlab sudo gitlab-ctl stop 2. 卸载…

Linux快速入门-Linux的常用命令

Linux的常用命令 1. Linux的终端与工作区1.1 终端概述1.2 切换终端 2. Shell语言解释器2.1 Shell概述 3. 用户登录与身份切换3.1 su 命令3.2 sudo 命令 4. 文件、目录操作命令4.1 pwd 命令4.2 cd 命令4.3 ls 命令4.3.1 ls 指令叠加使用 4.4 mkdir 命令4.5 rmdir 命令4.6 cp 命令…

三、ubuntu18.04安装docker

1.使用默认ubuntu存储库安装docker 更新软件存储库 更新本地软件数据库确保可以访问最新版本。打开终端输入&#xff1a;sudo apt-get update 卸载旧版本的docker 建议继续之前卸载任何旧的docker软件。打开终端输入&#xff1a;sudo apt-get remove docker docker-engine …

【Linux系统编程】:信号(2)——信号的产生

1.前言 我们会讲解五种信号产生的方式: 通过终端按键产生信号&#xff0c;比如键盘上的CtrlC。kill命令。本质上是调用kill()调用函数接口产生信号硬件异常产生信号软件条件产生信号 前两种在前一篇文章中做了介绍&#xff0c;本文介绍下面三种. 2. 调用函数产生信号 2.1 k…

专业电脑数据恢复软件 iFind Data Recovery v9.2.3 绿色便携版

前言 iFinD Data Recovery一款特别实用的数据找回工具&#xff0c;它很厉害&#xff0c;能帮你在SSD硬盘和Windows10系统上找回丢失的数据。而且&#xff0c;它还能深度扫描并恢复各种主流数码相机里的RAW格式照片&#xff0c;速度超快&#xff0c;用起来也很稳定顺畅&#xf…