Java的并发修改异常

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析

阶段4、深入jdk其余源码解析

阶段5、深入jvm源码解析

引出问题

public class ForeachTest {
 
    public static void main(String[] args) {
 
        List<String> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
 
        for (String value : list) {
            if ("b".equals(value)) {
                list.remove(value);
            }
        }
 
        System.out.println(list);
    }
}

运行上面的代码会发生什么?

大部分人的回答是:会发生并发修改异常(ConcurrentModificationException)。

其实上面的代码会正常输出[a, c],而且IDEA还提示我们有更好的写法:

也就是:

public class ForeachTest {
 
    public static void main(String[] args) {
 
        List<String> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
 		// 使用Java8新增的removeIf
        list.removeIf("b"::equals);
        System.out.println(list);
    }
}

现在摆在我们面前的问题变为:

  • 不是说增强for+list.remove()会发生并发修改异常吗?怎么上面的代码“安然无恙”?
  • 为什么推荐使用removeIf(),它好在哪?

在回答这两个问题之前,我们先复习一下什么是并发修改异常,以及什么时候会出现并发修改异常。

什么是并发修改异常,如何避免?

public class ForeachTest {
 
    public static void main(String[] args) {
 
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
 
        // 普通for
        plainForMethod(list);
        // 增强for,底层是迭代器
        foreachMethod(list);
    }
 
    private static void plainForMethod(List<Integer> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
 
    private static void foreachMethod(List<Integer> list) {
        for (Integer integer : list) {
            System.out.println(integer);
        }
    }
}

对上面的代码进行反编译:

你会发现增强for的底层就是Iterator,而Iterator的next()方法会检查并发修改异常,简而言之就是集合的“版本号”是否在遍历过程中发生了改变:

那么什么时候“版本号”modCount会改变呢?增删都会改变modCount的值(注意,删除也是modCount++,版本号只能递增):

了解了并发修改异常的原因后,我们再来看看如何避免它。对于List来说,有两种方法:

  • 迭代器迭代元素,迭代器修改元素(ListIterator)
  • 集合遍历元素,集合修改元素(for)

也就是说,用集合遍历时(普通for)就用集合的方法去修改,用迭代器遍历时就用迭代器自带的方法修改,不能混用。接下来,我们一起分析一下上面两种方法背后的原理。

用迭代器遍历元素,用迭代器修改元素

public class ForeachTest {
 
    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 value = iterator.next();
            if ("a".equals(value)) {
                iterator.remove();
            }
        }
        System.out.println(list); // 输出[b, c]
    }
}

为什么iterator.remove()不会引发ConcurrentModificationException呢?

正如刚才所说,增强for底层是Iterator,list.remove()会修改modCount,而Iterator的next()会去checkForComodification(),一旦modCount!=expectedModCount就会抛异常。

上面的代码中,next()仍在调用,也就是说还是List还是会在遍历时检查modCount,那么不发生并发修改异常的原因只有一个:

不同于List的remove,Iterator提供的remove等增改操作会让expectedModCount与modCount保持同步,即expectedModCount始终等于modCount。

这样一来,即使下一轮遍历next()内部仍旧检测版本号,但由于两个数值始终相等,所以不会抛异常。

普通for遍历List,然后通过List删除元素

集合遍历+集合删除则完全脱离了List版本号的约束:遍历用的是普通for循环,根本不会检测版本号,所以即使list.remove()确实把modCount改得面目全非,也不会被检测到。

public class ForeachTest {
 
    public static void main(String[] args) {
 
        List<String> list = new ArrayList<>();
        list.add("a");
        list.add("a");
        list.add("b");
        list.add("c");
 
        for (int i = list.size() - 1; i >= 0; i--) {
            if ("a".equals(list.get(i))) {
                list.remove(list.get(i));
            }
        }
        System.out.println(list);
    }
}

这里有一个细节,不知道大家是否注意到了:for循环是倒序的。

为什么倒序?因为顺序遍历时删除元素会有坑。

你会发现,当第一个a被删除后,会发生数组拷贝,后面的元素全部往前移动,而数组的指针(cursor)却往后移动,最终第二个a被跳过了。

如果非要正序遍历又想避免跳过,就要在每次删除元素后,都把for循环“往回拨一位”:

public class ForeachTest {
 
    public static void main(String[] args) {
 
        List<String> list = new ArrayList<>();
        list.add("a");
        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(list.get(i));
                i--; // 回拨指针
            }
        }
        System.out.println(list);
    }
}

回答开头的问题

一般来说,不建议使用增强for的同时用List#remove()移除元素,很大概率会发生并发修改异常。上面的代码是“凑巧”。

public class ForeachTest {
 
    public static void main(String[] args) {
 
        List<String> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
 
        for (String value : list) {
            // a或c都会抛异常
            if ("a".equals(value)) {
                list.remove(value);
            }
        }
        System.out.println(list);
    }
}

我们可以再做一个实验:

public class ForeachTest {
 
    public static void main(String[] args) {
 
        List<String> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
 
        for (String value : list) {
            // 只有c不会抛异常
            if ("c".equals(value)) {
                list.remove(value);
            }
        }
        System.out.println(list);
    }
}

看出问题了吗?

是的,只有倒数第二个才能“幸免于难”...

结合上面的内容,你应该已经猜到原因:

迭代的remove()底层是这样处理的:

  • 如果原本数组是[a,b,c]
  • 你移除了b,其实最终经过数组拷贝,会变成[a,c,c],也就是后面的部分元素往前挪了
  • 然后elementData[--size]会释放末尾那个元素,最终变成[a,c]

但问题在于迭代器此时再调用hasNext()时,确实没有元素了,因为刚才已经到第二个元素了,而现在只剩两个元素,所以会认为遍历结束了:

不调用next()意味着不会调用checkForComodification()去检查并发修改异常(虽然此时其实已经不一致)。

所以,这并不是JDK的bug,而是我们自己使用不当。迭代器遍历不应该使用List的remove,推荐interaror的remove。

至于IDEA为什么推荐使用List#removeIf(),我们可以看看removeIf()是怎么实现的:

底层其实就是迭代器遍历+迭代器remove,Iterator#remove()会把modCount重新赋值给expectedModCount,所以两者始终一致。

如果看完这篇文章你只能记住一件事,请记得:

需要删除List元素时,尽量使用迭代器,或者普通for倒序删除。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/290026.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

原生JS调用OpenAI GPT接口并实现ChatGPT逐字输出效果

效果&#xff1a; 猜你感兴趣&#xff1a;springbootvue实现ChatGPT逐字输出打字效果 附源码&#xff0c;也是小弟原创&#xff0c;感谢支持&#xff01; 没废话&#xff0c;上代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><me…

【Proteus仿真】【STM32单片机】超声波测距系统

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真STM32单片机控制器&#xff0c;使用动态数码管、按键、HCSR04超声波、蜂鸣器模块等。 主要功能&#xff1a; 系统运行后&#xff0c;数码管显示超声波检测距离&#xff0c;当检测…

奈奎斯特定理

奈奎斯特定理是通信领域中重要的理论基础之一&#xff0c;它对于数字通信系统中的信号采样和重构具有至关重要的作用。在数字信号处理和通信技术中&#xff0c;奈奎斯特定理的应用不仅具有理论意义&#xff0c;还对通信系统的设计、优化和性能提升起着重要的指导作用。本文将以…

8868体育助力意甲博洛尼亚俱乐部 主帅被评为最佳

博洛尼亚俱乐部是8868体育合作球队之一&#xff0c;本赛季在意甲联赛中表现出色&#xff0c;目前以8胜7平2负的成绩排名第四&#xff0c;积31分。意大利媒体评选出的年度最佳主帅是莫塔&#xff0c;本赛季莫塔率领博洛尼亚连续战胜强敌&#xff0c;目前在意甲积分榜上排名第四&…

进阶学习——Linux系统中重点‘进程’

目录 一、程序和进程的关系 1.程序 2.进程 2.1线程 2.2协程 3.进程与线程的区别 4.总结 4.1延伸 5.进程使用内存的问题 5.1内存泄漏——Memory Leak 5.2内存溢出——Memory Overflow 5.3内存不足——OOM&#xff08;out of memory&#xff09; 5.4进程使用内存出现…

Algorithm-Left Edge算法

算法输入&#xff1a; 多个段&#xff0c;每个段由两个值表示&#xff0c;例如&#xff08;1&#xff0c;3&#xff09; 算法原理&#xff1a; 将多个段按照左边的值排序放到列表中遍历列表&#xff0c;不断选择没有重叠的段&#xff0c;直到列表遍历结束&#xff0c;将选择…

fineBI web组件传参

1、fineBI web组件传参 1.1、 Web组件- FineBI帮助文档 FineBI帮助文档1. 概述1.1 版本FineBI 版本HTML5移动端展现功能变动6.0--V11.0.83web组件适配移动端效果优化6.0.13-web组件支持传递参数 ${过滤组件https://help.fanruan.com/finebi/doc-view-143.html 1.2、自己做的例…

Java 将Excel转换为TXT文本格式

TXT文件是一种非常简单、通用且易于处理的文本格式。在处理大规模数据时&#xff0c;将Excel转为TXT纯文本文件可以提高处理效率。此外&#xff0c;许多编程语言和数据处理工具都有内置的函数和库来读取和处理TXT文件&#xff0c;因此将Excel文件转换为TXT还可以简化数据导入过…

如何读取tif格式文件(基于PIL)

背景介绍 在许多机器学习的任务中&#xff0c;大多数图像类型的训练数据集会以tif的格式储存&#xff0c;在这种情况下&#xff0c;如何读取tif格式的数据就至关重要 tif格式 TIF&#xff08;Tagged Image File Format&#xff09;格式&#xff0c;也被称为TIFF&#xff0c;是…

基于Vue开发的一个仿京东电商购物平台系统(附源码下载)

电商购物平台项目 项目完整源码下载 基于Vue开发的一个仿京东电商购物平台系统 Build Setup # csdn下载该项目源码压缩包 解压重命名为sangpinghui_project# 进入项目目录 cd sangpinghui_project# 安装依赖 npm install# 建议不要直接使用 cnpm 安装以来&#xff0c;会有各…

生成式AI在自动化新时代中重塑RPA

生成式AI的兴起正在推动行业的深刻变革&#xff0c;其与RPA技术的结合&#xff0c;标志着自动化领域新时代的到来。这种创新性结合极大地提升了系统的适应性&#xff0c;同时也推动了高级自动化解决方案的发展&#xff0c;为下一代RPA的诞生奠定了坚实的基础。 核心RPA技术专注…

数据结构——二叉树四种遍历的实现

目录 一、树的概念 1、树的定义 1&#xff09;树 2&#xff09;空树 3&#xff09;子树 2、结点的定义 1&#xff09;根结点 2&#xff09;叶子结点 3&#xff09;内部结点 3、结点间关系 1&#xff09;孩子结点 2&#xff09;父结点 3&#xff09;兄弟结点 4、树…

船舶数据采集与分析在线能源监测解决方案

一、船舶在线能源监测应用前景 船舶在线能源监测在能源效率优化、故障诊断和预测维护、节能减排和环保监管、数据分析和决策支持以及自动化智能化等方面具有广阔的应用前景。随着船舶行业对能源管理和环保要求的不断提高&#xff0c;船舶在线能源监测技术将成为船舶运营和管理中…

华为端口隔离高级用法经典案例

最终效果&#xff1a; pc4不能ping通pc5&#xff0c;pc5能ping通pc4 pc1不能和pc2、pc3通&#xff0c;但pc2和pc3能互通 vlan batch 2 interface Vlanif1 ip address 10.0.0.254 255.255.255.0 interface Vlanif2 ip address 192.168.2.1 255.255.255.0 interface MEth0/0/1 i…

2020年认证杯SPSSPRO杯数学建模A题(第二阶段)听音辨位全过程文档及程序

2020年认证杯SPSSPRO杯数学建模 A题 听音辨位 原题再现&#xff1a; 把若干 (⩾ 1) 支同样型号的麦克风固定安装在一个刚性的枝形架子上 (架子下面带万向轮&#xff0c;在平地上可以被水平推动或旋转&#xff0c;但不会歪斜)&#xff0c;这样的设备称为一个麦克风树。不同的麦…

imgaug库指南(二):从入门到精通的【图像增强】之旅

文章目录 引言前期回顾代码示例小结结尾 引言 在深度学习和计算机视觉的世界里&#xff0c;数据是模型训练的基石&#xff0c;其质量与数量直接影响着模型的性能。然而&#xff0c;获取大量高质量的标注数据往往需要耗费大量的时间和资源。正因如此&#xff0c;数据增强技术应…

SpingBoot的项目实战--模拟电商【3.购物车模块】

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于SpringBoot电商项目的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.功能需求 二.代码编写 …

微服务-Gateway

案例搭建 官网地址 父Pom <com.alibaba.cloud.version>2.2.8.RELEASE</com.alibaba.cloud.version> <com.cloud.version>Hoxton.SR12</com.cloud.version> <com.dubbo.version>2.2.7.RELEASE</com.dubbo.version> <dependencyManagem…

文件摆渡系统如何实现网络隔离后的数据交换、业务流转?

近年来全球网络安全威胁态势的加速严峻&#xff0c;使得企业对于网络安全有了前所未有的关注高度。即便没有行业性的强制要求&#xff0c;但在严峻的安全态势之下&#xff0c;企业的网络安全体系建设正从“以合规为导向”转变到“以风险为导向”&#xff0c;从原来的“保护安全…

【操作系统xv6】学习记录--实验1 Lab: Xv6 and Unix utilities--未完

ref:https://pdos.csail.mit.edu/6.828/2020/xv6.html 实验&#xff1a;Lab: Xv6 and Unix utilities 环境搭建 实验环境搭建&#xff1a;https://blog.csdn.net/qq_45512097/article/details/126741793 搭建了1天&#xff0c;大家自求多福吧&#xff0c;哎。~搞环境真是折磨…