ArrayList遍历时结构修改会抛ConcurrentModificationException,单线程推荐Iterator.remove()、倒序for循环或先收集后批量删;多线程应选CopyOnWriteArrayList、synchronizedList或手动加锁;只读场景优先用不可变集合。

Java 中 ArrayList 本身不支持并发修改,只要在遍历过程中被结构化修改(比如 add、remove),就会抛 ConcurrentModificationException。这不是多线程专属问题——单线程里用 for-each 循环删元素也会触发。关键在于避开“迭代器检测到 modCount 不一致”这个机制,而不是等异常再捕获。
单线程下安全删除的三种写法
遍历时要删元素,不能直接调用 list.remove(),否则必报错。推荐以下方式:
-
用 Iterator.remove():这是唯一被允许的遍历中删除方式。它会同步更新
expectedModCount,避免检查失败。 -
倒序 for 循环删除:从
size-1往 0 遍历,删元素不影响后续索引定位,不走迭代器,自然绕过检查。 -
先收集后批量删:遍历时把待删元素加进临时 List,循环结束后调
list.removeAll(toRemove)。结构修改只发生一次,无冲突。
多线程环境必须换集合或加锁
ArrayList 本身无任何同步机制,多个线程同时读写必然出问题。强行 synchronized 包裹部分操作也不够——比如只锁了 add,但 for-each 遍历没锁,照样 CME。
- 读多写少选 CopyOnWriteArrayList:每次写都复制新数组,迭代器基于快照,完全不会抛 CME;缺点是内存开销大、写性能低、看不到实时更新。
-
读写较均衡用 Collections.synchronizedList:所有方法自动加锁,但注意遍历时必须手动同步整个块:
synchronized(list) { for (e : list) {...} },否则仍不安全。 -
需要精细控制就手动加锁:用同一个
ReentrantLock或synchronized(obj)包住所有对 list 的访问(含遍历、增、删、改),确保临界区互斥。
能不动就别动:改用只读或不可变方案
很多场景其实不需要运行时修改。如果列表初始化后就固定,比如配置项、状态码映射、常量集合,直接转成不可变集合最省心:
Java项目代码review工具。分析Git变更+完整调用链路上下文,推断业务需求,进行多维度评分和分类汇总,生成完整PRD文档。包含细粒度Java代码审查清单(Null安全、异常处理、Streams、并发、equals/hashCode、资源管理、API设计、性能、MyBatis/ORM、事务边界、SQL/DD...
立即学习“Java免费学习笔记(深入)”;
- JDK 10+ 用
List.copyOf(original) - Guava 用
ImmutableList.copyOf(list) - 这样既线程安全,又避免意外修改,还节省内存(共享底层数组)。
别踩增强 for 循环的坑
for-each 看似简洁,但底层就是 iterator。只要循环体里出现 list.add() 或 list.remove(),编译器不会报错,运行时一定崩。
想批量处理?优先用 JDK 自带的安全方法:list.removeIf(predicate)、list.replaceAll(),它们内部已协调好 modCount,不会触发异常。

















