ConcurrentModificationException未抛出不等于安全,它可能静默导致数据丢失、逻辑跳过或状态错乱;因快速失败机制仅在next()/hasNext()时校验modCount,修改发生在迭代末尾或中断后便不会触发异常。

ConcurrentModificationException 不一定抛出来,才是最危险的——它可能静默导致数据丢失、逻辑跳过或状态错乱。
为什么线上没报 ConcurrentModificationException 却出错了
异常未抛出 ≠ 没问题。Java 的快速失败机制只在迭代器调用 next() 或 hasNext() 时检查 modCount 与 expectedModCount 是否一致。如果修改发生在迭代末尾(比如刚遍历完最后一个元素后才调用 list.remove()),或者修改后迭代器不再调用 next()(如提前 break、return、异常中断),异常就不会触发,但集合已损坏。
- 单线程下也可能静默出错:例如
for (String s : list)中list.remove(s)删除的是倒数第二个元素,循环会直接结束,不触发检查 - 多线程下更隐蔽:一个线程在
iterator.hasNext()返回true后、next()执行前,被另一个线程修改了集合,此时next()可能返回错误元素、null,甚至不抛异常(取决于具体集合实现和时机) -
CopyOnWriteArrayList迭代时不抛异常,但读到的是快照——你删了元素,迭代器仍能看到它;你加了元素,迭代器看不到——这种“一致性幻觉”比崩溃更难排查
日志里根本搜不到 ConcurrentModificationException 怎么定位
别只盯着异常栈。这类问题往往表现为业务逻辑结果异常:比如批量处理本该删 5 条记录,实际只删了 3 条;通知列表漏发某条消息;状态机卡在中间态。排查要从“结果反推行为”:
- 在集合操作前后打结构快照日志:
log.debug("list size before iterate: {}, content: {}", list.size(), list),注意避免toString()触发额外迭代 - 对关键集合启用 JVM 参数
-Dsun.misc.Unsafe.allowed=true(仅调试环境)配合 JFR(Java Flight Recorder)录制jdk.CollectionStatistics事件,观察modCount被修改的线程和堆栈 - 用
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005启动线上(灰度)实例,配合 IDE 条件断点:在ArrayList$Itr.checkForComodification方法内设断点,条件为modCount != expectedModCount,抓取真实并发路径
parallelStream + 非线程安全集合是高频雷区
这是线上偶发问题最典型的组合。错误不是立刻爆炸,而是随数据量、CPU 核心数、JVM 调度随机暴露:
- 错误写法:
dataList.parallelStream().forEach(vo -> successList.add(vo)),其中successList是new ArrayList()——add()不是原子操作,多个线程同时扩容、写elementData数组会导致数据覆盖或ArrayIndexOutOfBoundsException - 正确做法:要么改用
CopyOnWriteArrayList(小数据量、读多写少),要么用collect(Collectors.toList())让并行流自己聚合,或用stream().collect(Collectors.groupingBy(...))分离关注点 - 验证手段:本地用
ForkJoinPool.commonPool().setParallelism(8)强制高并发,配合Thread.sleep(1)在 add 前插入延迟,极大提升复现概率
最容易被忽略的“伪安全”写法
有些写法看似绕过了异常,实则埋下更大隐患:
-
for (int i = list.size() - 1; i >= 0; i--) { if (needRemove(list.get(i))) list.remove(i); }—— 反向遍历确实不会触发CME,但如果其他线程同时在正向遍历或添加,size()和get(i)仍可能读到不一致状态 -
synchronized (list) { for (String s : list) { ... } }—— 锁住了遍历,但若锁粒度太粗(比如整个方法加锁),会严重拖慢吞吐;若锁粒度太细(只锁遍历块),其他线程仍可在锁外修改集合 - 用
Vector或Hashtable替代——它们的方法全加了synchronized,但性能极差,且不能解决迭代器与修改混用的逻辑错误
真正安全的边界只有一条:**同一时刻,集合要么被迭代,要么被修改,不能两者共存。** 多线程场景下,这个约束必须靠显式同步、不可变集合或并发集合的语义来保障,而不是靠“没报错”来确认。

















