java遍历中修改集合会触发fail-fast机制抛concurrentmodificationexception,正确做法是用iterator.remove()、倒序for循环或removeif等安全方法,多线程下需按场景选容器。

遍历过程中修改集合,是Java里最典型的“看着没问题、一跑就崩”场景。根本原因不是多线程,而是集合自身的fail-fast机制主动拦截了不一致的操作——它用modCount和expectedModCount做校验,一旦发现结构被意外改动,立刻抛ConcurrentModificationException。安全操作的关键,在于尊重迭代契约,而不是绕过报错。
增强for循环里别直接调remove()
这是最常踩的坑:写个for (String s : list),里面顺手list.remove(s),运行直接崩溃。因为增强for底层就是Iterator,而list.remove()会触发modCount变更,导致下一次next()前校验失败。
- ❌ 错误示范:
for (String s : list) { if (s.startsWith("A")) list.remove(s); } - ✅ 正确做法:改用显式Iterator,并只调
it.remove() - ⚠ 注意:每个
next()后只能调一次remove(),重复调会抛IllegalStateException
普通for循环删元素要倒着遍历
用索引遍历看似绕开了Iterator,但list.remove(i)会让后面元素前移,导致跳过下一个元素。比如删掉索引2的元素后,原索引3的元素变成新索引2,而循环i已递增到3,这个元素就被跳过了。
- ❌ 危险写法:
for (int i = 0; i - ✅ 安全写法:从尾部开始
for (int i = list.size() - 1; i >= 0; i--),删完不影响前面索引 - ? 补充:如果必须正向遍历,可配合
i--回退一步,但逻辑易错,不推荐
批量操作优先用内置方法
Java 8+提供了语义清晰又内部安全的批量处理方法,它们封装了正确的迭代逻辑,避免手写样板代码出错。
-
list.removeIf(predicate):一次性删掉所有匹配项,内部用Iterator安全删除 -
list.replaceAll(unaryOperator):安全替换所有元素,不触发结构修改检测 -
list.stream().filter(...).collect(Collectors.toList()):生成新列表,原列表不动,彻底规避问题
多线程场景不能只换容器
看到CME就加CopyOnWriteArrayList,容易掉进新坑。它确实不抛异常,但每次写都复制整个数组,写频繁时GC压力大;且迭代器基于快照,遍历时新增/删除的元素对本次循环不可见。
- ✅ 合适场景:监听器列表、配置白名单等读远多于写的场合
- ❌ 不合适场景:订单列表、实时状态缓存等需要强一致性或高频写入的业务
- 替代方案:用
Collections.synchronizedList()+ 外层手动同步块,或改用ConcurrentHashMap分段锁机制











