JDK并发修改异常的一个“BUG“

很多电商公司早期的架构都是基于PHP,所以我身边会有很多很厉害的PHP老哥,但现在都在写Java。昨天看到他在看Java的并发修改异常,正打算秀一波操作,却被他的一个问题难住了:

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);}
}

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

复习并发修改异常

先留个悬念,我们先来复习一下并发修改异常是怎么回事:

public class ForeachTest {public static void main(String[] args) {List<Integer> list = new ArrayList<>();list.add(1);list.add(2);list.add(3);// 普通forplainForMethod(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);}}
}

直接用IDEA打开源码:

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

那么什么时候“版本号”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]}
}

安全的删除办法二(养兔子的大叔提供):普通for遍历List,然后通过List删除元素

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);}
}

这里有一个细节,不知道大家是否注意到了:养兔子的大叔是倒序遍历的。

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

你会发现,当第一个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);}
}

观察JDK的Iterator的BUG

OK,现在让我们回到开头的问题。

一般来说,不建议使用增强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会提示我们更优的写法:

底层其实就是迭代器。

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

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

相关文章

海上船舶交通事故VR模拟体验低成本高效率-深圳华锐视点

在海上运输行业&#xff0c;安全事故的防范和应对能力是企业安全教育的重中之重。针对这一问题&#xff0c;海上运输事故VR模拟逃生演练成为了一种创新且高效的教育手段。通过这种演练&#xff0c;企业能够在提升员工安全意识和技能方面获得多方面的帮助。 在VR船舶搜救演练中&…

C语言--假设共有鸡、兔30只,脚90只,求鸡、兔各有多少只​

一.题目描述 假设共有鸡、兔30只&#xff0c;脚90只&#xff0c;求鸡、兔各有多少只&#xff1f; 二.思路分析 本题是一个典型的穷举法例题&#xff0c;而穷举法&#xff0c;最重要的就是条件判断。⭐⭐ 本题中的条件很容易发现&#xff1a; 假设鸡有x只&#xff0c;兔有y只…

windows安装composer并更换国内镜像

第一步、官网下载 下载地址 Composer安装https://getcomposer.org/Composer-Setup.exe第二步、双击安装即可 第三步选择 php安装路径并配置path 第四步、 composer -v查看安装是否成功&#xff0c;出现成功界面 第五步、查看镜像地址并更换&#xff08;composer国内可能较慢…

Django中如何创建表关系,请求生命周期流程图

Django中ORM创建表关系 如何创建表关系(一对一 &#xff0c; 一对多 &#xff0c; 多对多) 图书表&#xff0c;出版社表&#xff0c;作者表&#xff0c;作者详情表 换位思考法判断表关系 图书表和出版社表 >>> 一对多 >>> 图书表是多&#xff0c;出…

Spring Cloud LoadBalancer基础知识

LoadBalancer 概念常见的负载均衡策略使用随机选择的负载均衡策略创建随机选择负载均衡器配置 Nacos 权重负载均衡器创建 Nacos 负载均衡器配置 自定义负载均衡器(根据IP哈希策略选择)创建自定义负载均衡器封装自定义负载均衡器配置 缓存 概念 LoadBalancer(负载均衡器)是一种…

SAP系统供应商预付款请求和预付账款业务

最近搞清帐&#xff01; 在SAP中处理客户或供应商的预收/预付款相关业务流程操作说明, 首先由业务部门(销售或采购)下达销售/采购订单,同时基于订单提交预收/预付申请,客户/供应商款项到账时,由财务部门在SAP中勾选申请单来收付款;最后在财务转应收/应付转发票时自动核销。预付…

找工作在哪个app找比较真实可靠

吉鹿力招聘网是一款找工作比较真实靠谱的app。吉鹿力招聘网是一个新兴的人脉社交招聘平台&#xff0c;靠谱而且需求明确&#xff0c;可以依靠自己或者身边朋友推荐。在吉鹿力招聘网上可以有很多前辈的职场分享和行业八卦&#xff0c;对于刚毕业的大学生而言&#xff0c;很有参考…

科普测量开关电源输出波形的三种方法及电源波形自动化测试步骤

开关电源波形测试就是对开关电源的输出波形进行检测和分析&#xff0c;观察开关电源参数变化&#xff0c;以此来判断开关电源的性能是否符合要求。好的开关电源对于设备以及整个电路的正常运行是非常重要的&#xff0c;因此开关电源输出波形测试是开关电源测试的重要环节&#…

交叉编译 openssl

要在 x86 平台上编译适用于 aarch64 架构的 OpenSSL 动态库&#xff0c;你需要使用交叉编译工具链。可以按照以下步骤进行&#xff1a; 安装 aarch64 交叉编译工具链&#xff1a; $ sudo apt-get install gcc-aarch64-linux-gnu g-aarch64-linux-gnu 这将安装 aarch64 交叉编…

Kafka入门

kafka无疑是当今互联网公司使用最广泛的分布式实时消息流系统&#xff0c;它的高吞吐量&#xff0c;高可靠等特点为并发下的大批量实时请求处理提供了可靠保障。很多同学在项目中都用到过kafka&#xff0c;但是对kafka的设计原理以及处理机制并不是十分清楚。为了知其然知其所以…

ubuntu16.04 交叉编译 mbedtls

在为客户交叉编译项目时需要依赖 mbedtls&#xff0c; 客户的机器是 arm64 的 ubuntu 16.04&#xff0c; 交叉编译过程中遇到几个问题。 首先&#xff0c; mbedtls 需要依赖 python, 在 cmake 的过程中&#xff0c; 如果不是使用系统默认的 cmake 可能会导致&#xff0c;mbedt…

【JavaEE】Servlet(创建Maven、引入依赖、创建目录、编写及打包、部署和验证、smart Tomcat)

一、什么是Servlet&#xff1f; Servlet 是一种实现动态页面的技术. 是一组 Tomcat 提供给程序猿的 API, 帮助程序猿简单高效的开发一个 web app 1.1 Servlet能干什么&#xff1f; &#x1f695;允许程序猿注册一个类, 在 Tomcat 收到某个特定的 HTTP 请求的时候, 执行这个类…