1. 名词与本质
-
迭代安全(iteration safety) :在遍历期间,容器的结构是否允许被修改,以及在多线程读写时,遍历能否保持一致的语义(不崩不乱)。
-
fail-fast 迭代器:当检测到“结构性修改”(结构上增/删元素)且不是由当前迭代器执行时,在下次 next()/hasNext() 处抛 ConcurrentModificationException(CME)。这是“尽早发现问题”的调试信号,不是并发安全保证。
典型 fail-fast 容器:ArrayList、LinkedList、HashMap…
典型 非 fail-fast(或 弱一致/快照)容器:CopyOnWriteArrayList(快照)、ConcurrentLinkedQueue、ConcurrentHashMap 的迭代器(弱一致)。
2. fail-fast 的实现原理(以 ArrayList/LinkedList 为例)
-
每个容器维护一个 modCount(结构修改计数)。
-
迭代器创建时保存 expectedModCount = modCount。
-
每次 next()/hasNext()/forEachRemaining() 会校验:若 modCount != expectedModCount → 抛 CME。
-
例外:当前迭代器自己的 remove()/ListIterator.add()/set() 会同步更新 expectedModCount,因此允许在遍历中安全地修改。
结构性修改 指改变元素数量或内部结构(增/删/清空等);set(i, x) 不改变结构,不算结构修改。
3. 最常见“踩坑”与修复
坑 1:for-each 中直接对 List 做增删
// ❌ 可能 CME
for (String s : list) {
if (s.startsWith("x")) list.remove(s);
}
修复:用迭代器的 remove() 或 removeIf(JDK8+ 内部使用迭代器语义)
// ✅ 方式 A:显式迭代器
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
if (it.next().startsWith("x")) it.remove(); // 安全,更新 expectedModCount
}
// ✅ 方式 B:批量删除(更简洁)
list.removeIf(s -> s.startsWith("x"));
坑 2:subList 与原 List 交叉修改
List<Integer> sub = list.subList(10, 20);
sub.clear(); // OK(通过 subList 修改,modCount同步)
list.add(999); // ❌ 再访问 sub 迭代器 → CME
修复:要么只修改 subList,要么用独立副本:
List<Integer> slice = new ArrayList<>(list.subList(10, 20)); // 快照
坑 3:Collections.synchronizedList 遍历未加锁
List<Integer> sync = Collections.synchronizedList(new ArrayList<>());
for (Integer x : sync) { // ❌ 只锁了结构操作,遍历仍需外部同步
...
}
修复:文档要求遍历时也需同步同一把锁:
synchronized (sync) {
for (Integer x : sync) { ... }
}
坑 4:多线程写 + fail-fast****
-
fail-fast 不是并发控制,它只是“尽力”报警;未同步的并发修改可能看不到从而不抛,导致脏读/遗漏。
-
正确做法:用并发容器(弱一致)或外部同步/串行化。
4. 正确改法清单(单线程遍历)
-
单线程遍历中需要删:用 Iterator.remove() 或 ListIterator 的 add/remove/set。
-
大量条件删除:removeIf(底层走迭代器,安全且可读)。
-
需要边遍历边插入到中间:ListIterator.add() (只能对当前位置附近,且复杂度 O(n))。
5. 跨线程场景的三条道路
-
外部同步(强一致)
- 一把锁保护“遍历 + 修改”:
List<T> list = Collections.synchronizedList(new ArrayList<>());
synchronized (list) {
for (T t : list) { ... } // 读
list.add(x); list.remove(y); // 写
}
-
-
优点:一致性强;缺点:并发度低,容易形成“大锁”。
-
-
快照/副本遍历(读多写少)
- CopyOnWriteArrayList:遍历走旧数组快照,不抛 CME;写时复制开销大,适合监听器列表。
CopyOnWriteArrayList<Listener> ls = new CopyOnWriteArrayList<>();
for (Listener l : ls) l.onEvent(e); // 永不 CME
-
-
或者遍历前自己做快照:
-
for (T t : new ArrayList<>(list)) { ... } // 读快照,写原表互不干扰
-
并发容器(弱一致) (读写并行)
-
例如 ConcurrentLinkedQueue、ConcurrentHashMap 的迭代器弱一致:不抛 CME,能看到遍历开始后的一些改动,也可能看不到全部改动;按需保证即可。
-
注意:JDK 没有“并发 List”的理想实现;队列/栈语义尽量改数据结构(如 ConcurrentLinkedQueue)或用消息/事件流替代共享 List。
-
6. Kotlin 小贴士
- Kotlin MutableList 默认实现仍是 java.util.ArrayList(fail-fast 规则完全相同)。
- forEach {} / for (e in list) 背后也是迭代器;在 lambda 中直接 list.remove(e) 仍会 CME。
- 需要删就用:
val it = list.iterator()
while (it.hasNext()) if (needRemove(it.next())) it.remove()
// 或
list.removeAll { needRemove(it) }
7. 选择建议(面试/实战可直接背)
-
业务列表:默认 ArrayList;遍历删用 removeIf/迭代器;避免 subList 与父表交叉改。
-
队列/栈:ArrayDeque,无 fail-fast 问题(不支持中间插入/下标)。
-
监听器/读多写少:CopyOnWriteArrayList(快照、不抛 CME)。
-
多线程同时读写且需吞吐:改为并发容器或消息队列;不要指望 fail-fast 解决并发安全。
-
需要强一致:synchronizedList + 同锁遍历。
8. 小抄(TL;DR)
-
fail-fast = 发现“非本迭代器的结构修改”就抛 CME;只是早失败机制,非并发控制。
-
在遍历中修改,用迭代器提供的改法(remove/ListIterator.add);或用 removeIf。
-
多线程遍历:要么加锁,要么用快照,要么换并发容器/消息化。
-
subList 易踩坑:修改父表后再用子表迭代 → CME;需要就用独立副本。