add()遍历时必报ConcurrentModificationException,因迭代器的expectedModCount与集合modCount不一致触发fail-fast机制;安全做法包括暂存后批量添加、倒序遍历、ListIterator.add()或改用CopyOnWriteArrayList等。

直接在遍历中调用 list.add() 会触发 ConcurrentModificationException,这不是线程安全问题,而是单线程下“结构修改破坏迭代器一致性”的必然结果。
为什么 add() 在遍历时一定会报错
ArrayList 的迭代器(Itr)在创建时会记录一个 expectedModCount,它等于当前集合的修改计数器 modCount。每次调用 add()、remove() 等结构性修改方法,modCount 就会自增;而每次调用 iterator.next() 前,都会执行 checkForComodification() —— 若两者不等,立刻抛异常。
这个机制叫 fail-fast,目的不是防多线程,而是快速暴露逻辑错误。
-
foreach循环底层就是迭代器,所以for (String s : list) { list.add("x"); }同样崩 - 即使只读取元素,只要中途有
add(),下一次next()就失败(不一定在add()那行报错) -
modCount不跟踪元素值变化,只跟踪 size 或数组扩容等结构性变更
替代 add() 的安全写法(单线程场景)
不能边遍历边改原集合,但可以换种方式达成目标:先收集要加的元素,再统一追加。
- 用临时容器暂存新元素:
List<string> toAdd = new ArrayList();</string>,遍历中toAdd.add("白龙马"),遍历完再list.addAll(toAdd) - 倒序遍历 + 索引操作(仅适用于
ArrayList):for (int i = list.size() - 1; i >= 0; i--) { if ("猪八戒".equals(list.get(i))) list.add("白龙马"); }—— 注意这会把新元素加到末尾,不影响前面索引,但语义可能和预期不符 - 用
removeIf()或replaceAll()等函数式方法(Java 8+),它们内部已处理了迭代器一致性,但不支持add(),只适合删/改
需要实时插入且保持顺序?用 ListIterator
ListIterator 是唯一允许在遍历中安全插入的迭代器,但它要求你用它自己的 add() 方法,而不是集合的。
ListIterator<String> lit = list.listIterator();
while (lit.hasNext()) {
String s = lit.next();
if ("猪八戒".equals(s)) {
lit.add("白龙马"); // ✅ 安全,会自动更新 modCount 和游标
}
}
注意:lit.add() 插入位置在当前游标前(即刚遍历过的元素之后),且仅对 ArrayList、LinkedList 等支持双向迭代的实现有效;CopyOnWriteArrayList 虽然也允许遍历时 add(),但它是靠复制整个数组实现的,不适合高频写场景。
多线程环境下的 add() 遍历混用
如果遍历和 add() 来自不同线程,上面所有单线程方案都不够。此时必须换容器或加锁:
-
CopyOnWriteArrayList:读操作无锁,写操作复制数组。适合读多写少,且能容忍遍历看到“旧快照” -
Collections.synchronizedList(new ArrayList()):所有方法加synchronized,但遍历仍需手动同步块包裹,否则仍可能出错 -
ConcurrentLinkedQueue或ConcurrentLinkedDeque:无界、非阻塞,但不保证遍历顺序与插入顺序严格一致
最易被忽略的一点:哪怕用了线程安全集合,若在遍历中调用其 add(),仍可能因设计语义差异导致逻辑错乱(比如 CopyOnWriteArrayList 的迭代器永远看不到本次添加),所以优先考虑“分离读写”而非强行混用。

















