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.rhkb.cn/news/191086.html

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

相关文章

数据分析-numpy

numpy numpy numpy简介优点下载ndarray的属性输出数据类型routines 函数ndarray对象的读写操作ndarray的级联和切分级联切分 ndarray的基本运算广播机制&#xff08;Broadcast&#xff09;ndarry的聚合操作数组元素的操作numpy 数学函数numpy 查找和排序 写在最后面 简介 nump…

CSS 滚动捕获 scroll-snap-type

scroll-snap-type 语法实例 捕获轴 y 捕获严格程度 mandatory捕获轴 y 捕获严格程度 proximity同理看下捕获轴 x 一些注意事项兼容性 scroll-snap-type 用来指定一个滚动容器(scroll container)是否是滚动捕获容器(scroll snap container)、捕获的严格程度以及在什么方向上执行…

61基于matlab的GWO算法的参数工具箱,图形界面,目标函数的默认名称为CostFunction。

基于matlab的GWO算法的参数工具箱&#xff0c;图形界面&#xff0c;目标函数的默认名称为CostFunction。如果您查看了CostFunction.m文件&#xff0c;成本函数获取向量&#xff08;[x1 x2…xn]&#xff09;中的变量并返回目标值。可以在该文件中编写目标函数&#xff0c;也可以…

【计算机网络笔记】IP子网划分与子网掩码

系列文章目录 什么是计算机网络&#xff1f; 什么是网络协议&#xff1f; 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能&#xff08;1&#xff09;——速率、带宽、延迟 计算机网络性能&#xff08;2&#xff09;…

Perl爬虫程序的框架

Perl爬虫程序的框架&#xff0c;这个框架可以用来爬取任何网页的内容。 perl #!/usr/bin/perl use strict; use warnings; use LWP::UserAgent; use HTML::TreeBuilder; # 创建LWP::UserAgent对象 my $ua LWP::UserAgent->new; # 设置代理信息 $ua->proxy(http, ); …

ZooKeeper+Kafka+ELK+Filebeat集群搭建实现大批量日志收集和展示

大致流程&#xff1a;将nginx 服务器&#xff08;web-filebeat&#xff09;的日志通过filebeat收集之后&#xff0c;存储到缓存服务器kafka&#xff0c;之后logstash到kafka服务器上取出相应日志&#xff0c;经过处理后写入到elasticsearch服务器并在kibana上展示。 一、集群环…

Python实现WOA智能鲸鱼优化算法优化BP神经网络分类模型(BP神经网络分类算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 鲸鱼优化算法 (whale optimization algorithm,WOA)是 2016 年由澳大利亚格里菲斯大学的Mirjalili 等提…

web3 React dapp进行事件订阅

好啊&#xff0c;上文web3 React Dapp书写订单 买入/取消操作 我们已经写好了 填充和取消订单 这就已经是非常大的突破了 但是 留下了一个问题 那就是 我们执行完之后 订单的数据没有直接更新 每次都需要我们手动刷新 才能看到结果 那么 今天我们就来看解决这个问题的事件订阅 …

使用 `open-uri.with_proxy` 方法打开网页

Ruby 爬虫程序如下&#xff1a; require open-uri require nokogiri# 定义代理信息 proxy_host jshk.com.cn# 定义要爬取的网页 URL url http://www.example.com# 使用代理信息打开网页 open-uri.with_proxy(proxy_host, proxy_port) do |proxy|# 使用 Nokogiri 库解析网页内…

C++ 11 新特性

目录 1. 支持特性的编译器版本2. 模板表达式中空格3. 空指针4. auto5. 统一初始化6. explict7. 范围for8. default&#xff0c;delete9. 化名模板&#xff08;alias template&#xff09;10. using11. noexcept12. override13. final14. decltype15. lambda16. Variadic Templa…

Unity Hub无法登陆的两种终极解决办法

最近换了个电脑&#xff0c;需要重装Unity&#xff0c; 然后unity hub 怎么都无法登陆&#xff0c;登陆不了就不能激活personal license。试了很多次&#xff0c;包括unity hub 2.5.8 和unity hub 3.3都不行&#xff0c;真的是很崩溃。因为是公司的电脑&#xff0c;限制比较多&…

Android 基本属性绘制文本对象FontMetrics

FontMetrics对象 它以四个基本坐标为基准&#xff0c;分别为&#xff1a; ・FontMetrics.top ・FontMetrics.ascent ・FontMetrics.descent ・FontMetrics.bottom 如图: 要点如下&#xff1a; 1. 基准点是baseline 2. Ascent是baseline之上至字符最高处的距离 3. Descent是ba…

聚观早报 |京东11.11公布成绩单;2023数字科技生态大会

【聚观365】11月13日消息 京东11.11公布成绩单 2023数字科技生态大会 TikTok深受英国中小企业青睐 周鸿祎称大模型2年内可“进”智能汽车 双11全国快递业务量达 6.39 亿件 京东11.11公布成绩单 京东11.11公布成绩单&#xff1a;截至11月11日晚23:59&#xff0c;2023年京东…

【Kettle实战】数据分批处理及参数化传递子作业任务

对于大表操作&#xff0c;本来离线数据需要分批处理&#xff0c;刚开始只会用具体日期去做&#xff0c;通过复制多分转换和作业来处理。当日期范围大了后&#xff0c;这是个苦力活儿&#xff0c;kettle里面有参数化传递功能&#xff0c;多动手实操&#xff0c;懂得灵活变通自然…

2023数字科技生态展,移远通信解锁新成就

11月10日&#xff0c;以“数字科技&#xff0c;焕新启航”为主题的中国电信2023数字科技生态大会暨2023数字科技生态展在广州盛大启幕。作为物联网行业的龙头标杆&#xff0c;同时更与中国电信连续多年维持稳定友好的合作关系&#xff0c;移远通信受邀参加本次展会。 在本次展会…

Rust 中的引用与借用

目录 1、引用与借用 1.1 可变引用 1.2 悬垂引用 1.3 引用的规则 2、slice 类型 2.1 字符串字面量其实就是一个slice 2.2 总结 1、引用与借用 在之前我们将String 类型的值返回给调用函数&#xff0c;这样会导致这个String会被移动到函数中&#xff0c;这样在原来的作用域…

Java设计模式-结构型模式-代理模式

代理模式 代理模式静态代理动态代理JDK动态代理CGlib动态代理 代理模式 创建一个代理对象来控制对原始对象的访问&#xff0c;可以用来扩展原始对象的功能&#xff0c;同时保护原始对象 一般使用代理模式的目的有两个&#xff1a; 保护目标对象增强目标对象 代理模式有两种实现…

MATLAB | 官方举办的动图绘制大赛 | 第一周赛情回顾

嘿真的又是很久没见了&#xff0c;最近确实有点非常很特别小忙&#xff0c;今天带来一下MATHWORKS官方举办的迷你黑客大赛第三期(MATLAB Flipbook Mini Hack)的最新进展&#xff01;&#xff01;目前比赛已经刚好进行了一周&#xff0c;前两届都要求提交280个字符内的代码来生成…

JVM字符串常量池StringTable

目录 一、StringTable为什么要调整 二、String的基本特性 三、String的内存分配 四、字符串拼接操作 五、intern()方法 六、Stringtable的垃圾回收 七、G1中String去重操作 一、StringTable为什么要调整 jdk7之前&#xff0c;hotspot对于方法区的实现是永久代&#xff…

尝试使用php给pdf添加水印

在开发中增加pdf水印的功能是很常见的&#xff0c;经过实验发现这中间还是会有很多问题的。第一种模式&#xff0c;采用生成图片的方式把需要添加的内容保存成图片&#xff0c;再将图片加到pdf中间&#xff0c;这种方法略麻烦一些&#xff0c;不过可以解决中文乱码的问题&#…