新闻详情

新闻详情

首页 / 资讯中心 / 详情

Java 遍历时删元素抛 ConcurrentModificationException:fail-fast 原理与三种正确删法

发布时间:2026/9/1 3:02:27来源:尧图网络
Java 遍历时删元素抛 ConcurrentModificationException:fail-fast 原理与三种正确删法
Java 遍历时删元素抛 ConcurrentModificationException:fail-fast 原理与三种正确删法线上有段代码,把一批订单里已经取消的挑出来删掉,平时跑得好好的,某天量一大就抛ConcurrentModificationException,而且异常里没有任何和「并发」相关的线索——明明整段代码就一个线程在跑。这个异常名字里带 Concurrent,却经常在单线程里出现,坑了不少人。这篇把它的来龙去脉讲清楚,再给三种能落地的正确删法。先复现:一段看着人畜无害的删除代码importjava.util.*;publicclassDemo{publicstaticvoidmain(String[]args){ListStringordersnewArrayList(List.of(A,B-cancel,C,D-cancel));for(Stringo:orders){if(o.endsWith(-cancel)){orders.remove(o);// 直接在增强 for 里删}}System.out.println(orders);}}跑一下,直接抛异常:Exception in thread main java.util.ConcurrentModificationException at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013) at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)注意异常栈的最后一帧是Itr.next——问题不出在remove,而出在下一次取元素的时候。这说明增强 for 底层其实是迭代器在干活。原理:modCount 和 expectedModCount 对不上了增强 for 循环for (String o : orders)会被编译器展开成迭代器写法:IteratorStringitorders.iterator();while(it.hasNext()){Stringoit.next();// ...}ArrayList内部维护一个字段modCount,每次结构性修改(add/remove 导致 size 变化)都会让它 1。而迭代器在创建时,会把当时的modCount抄一份存成expectedModCount。每次调it.next()前,都会做一次检查:// ArrayList 内部源码,简化版finalvoidcheckForComodification(){if(modCount!expectedModCount)thrownewConcurrentModificationException();}当你用orders.remove(o)删元素时,改的是 list 的modCount,但迭代器手里那份expectedModCount没跟着变。下一次next()一检查,两个数对不上,直接抛异常。这套机制叫fail-fast:一旦发现集合在迭代过程中被意外结构性修改,立刻失败,而不是继续跑出一个谁也说不清的错误结果。所以它和多线程没有必然关系——单线程里自己一边遍历一边删,同样触发。多线程只是另一种「迭代期间被人改了」的场景而已。一个更隐蔽的坑:删倒数第二个元素时不抛异常fail-fast 是「尽力而为」,不是「保证一定抛」。看这段:ListStringlistnewArrayList(List.of(A,B));for(Stringo:list){if(o.equals(A)){list.remove(o);// 删的是倒数第二个}}System.out.println(list);// [B],居然正常跑完,没抛异常为什么?ArrayList的迭代器hasNext()判断依据是cursor ! size。删掉 “A” 后 size 从 2 变成 1,此时 cursor 也正好是 1,hasNext()返回 false,循环直接结束,压根没机会走到下一次next()的检查。这就是最危险的地方:你的测试用例可能恰好删的是倒数第二个,跑得好好的;上线后数据一变,删了别的位置,才炸。别指望「没抛异常」就等于「删对了」。正确删法一:用迭代器自己的 remove()Iterator提供了remove()方法,它删元素的同时会把expectedModCount同步成新的modCount,两边始终一致:IteratorStringitorders.iterator();while(it.hasNext()){Stringoit.next();if(o.endsWith(-cancel)){it.remove();// 用迭代器的 remove,不是 list 的}}要点:it.remove()删的是「上一次next()返回的那个元素」,所以必须先next()再remove(),连续调两次remove()会抛IllegalStateException。这是最经典、兼容性最好的写法,JDK 1.2 就有了。正确删法二:removeIf(Java 8 起,首选)如果只是「按条件删」,别再手写迭代器了,removeIf一行搞定,而且底层帮你处理好了 modCount:orders.removeIf(o-o.endsWith(-cancel));它内部是遍历一遍、标记要删的位置、最后统一搬移数组,只在结束时更新一次modCount,既不会抛 CME,性能也比「循环里一个个 remove」好——后者每删一个都要把后面的元素整体前移一位,是 O(n²)。日常需求 90% 都该用这个。正确删法三:CopyOnWriteArrayList(真·多线程场景)前两种解决的是「单线程边遍历边删」。如果是真的多线程——一个线程在遍历、另一个线程在增删,那removeIf也救不了你,得换容器。CopyOnWriteArrayList的迭代器基于「创建迭代器那一刻的数组快照」,遍历期间别的线程怎么改都不影响这份快照,自然不会 fail-fast:importjava.util.concurrent.CopyOnWriteArrayList;ListStringordersnewCopyOnWriteArrayList(List.of(A,B-cancel,C));// 线程 1 遍历for(Stringo:orders){System.out.println(o);// 遍历的是快照,不受线程 2 影响}// 线程 2 同时删,互不干扰orders.removeIf(o-o.endsWith(-cancel));代价是:每次写操作(add/remove)都会复制整个底层数组,所以它只适合「读远多于写」的场景,比如监听器列表、配置项列表。要是写很频繁,复制开销会把你拖垮,那种场景该考虑加锁或用别的并发容器。顺带说一句:HashMap 遍历时删也一样这套 fail-fast 不是 List 专属,HashMap、HashSet也一样。遍历 map 时想删 key,同样得用迭代器或removeIf:MapString,IntegermapnewHashMap(Map.of(a,1,b,0,c,2));// 删掉 value 为 0 的项map.entrySet().removeIf(e-e.getValue()0);// entrySet 的视图支持 removeIf小结ConcurrentModificationException由fail-fast机制抛出,核心是迭代器的expectedModCount和集合的modCount对不上,和多线程没有必然关系,单线程边遍历边删就会触发。它是「尽力而为」不是「保证抛」:删倒数第二个元素时不抛,别把「没报错」当成「删对了」。三种正确删法各有场景:单线程手动控制用Iterator.remove();按条件批量删用removeIf(首选,还快);真多线程并发读写用CopyOnWriteArrayList(读多写少)。一句话记忆点:遍历时想删东西,别碰集合本身,要么交给迭代器,要么交给 removeIf。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

雨夜模式与智能座舱:如何用摄像头和场景联动解决夜间视野难题 2026/9/1 3:38:33

雨夜模式与智能座舱:如何用摄像头和场景联动解决夜间视野难题

恶劣天气通勤是最容易暴露智能驾驶和座舱功能短板的时候。智己车型上提供的“雨夜模式”,针对的就是雨天夜里后视镜被雨水、雾气、眩光同时干扰,几乎看不清车侧路况的场景。很多人第一次听到这个名字,会以为只是一个屏幕亮度调节方案&#xf…

阅读更多 →
【单片机课设毕设项目】基于 STM32 的生理数据采集移动端远程查看系统设计 基于 STM32 的多传感器健康监护与声光预警系统实现(023705) 2026/9/1 3:38:33

【单片机课设毕设项目】基于 STM32 的生理数据采集移动端远程查看系统设计 基于 STM32 的多传感器健康监护与声光预警系统实现(023705)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

阅读更多 →
遥感图像语义分割实战:UNet与DeepLab V3+的PyTorch实现全解析 2026/9/1 3:38:33

遥感图像语义分割实战:UNet与DeepLab V3+的PyTorch实现全解析

简介:本资源是一套面向高校学生与遥感图像处理初学者的完整语义分割实践项目,聚焦遥感影像地物识别任务,提供基于PyTorch实现的DeepLab V3与U-Net双模型对比方案,适用于毕业设计、课程设计及期末大作业等学术场景。压缩包共11个文…

阅读更多 →
单片机毕业设计-基于 STM32 或 51 单片机的可查询取件记录寄存柜设计 基于 STM32 或 51 单片机的多功能密码智能储物系统设计(021905) 2026/9/1 3:38:33

单片机毕业设计-基于 STM32 或 51 单片机的可查询取件记录寄存柜设计 基于 STM32 或 51 单片机的多功能密码智能储物系统设计(021905)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

阅读更多 →
MySQL Connector/Python 2.1.7 源码包安装与实战全攻略 2026/9/1 3:38:33

MySQL Connector/Python 2.1.7 源码包安装与实战全攻略

简介:本资源是MySQL官方提供的Python数据库驱动程序mysql-connector-python 2.1.7版本源码包,面向Python开发者、后端工程师及数据库应用学习者,解决Python程序与MySQL数据库原生、合规交互的核心需求。压缩包共123个文件,含90个核…

阅读更多 →
UE5战斗系统开发实战:状态同步与事件驱动构建核心框架 2026/9/1 3:35:32

UE5战斗系统开发实战:状态同步与事件驱动构建核心框架

最近在社区里看到不少关于UE5战斗系统开发的讨论,很多朋友兴致勃勃地开始,却在实现连击、命中判定和伤害反馈这些核心环节时卡住。问题往往不是蓝图节点连不对,而是没想清楚这几个模块之间到底该怎么“对话”。比如,明明做了连击动…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞