一句话总结
fail-fast 通过 modCount 检测并发修改(迭代器创建时记录 expectedModCount,每次操作比对,不等则抛 ConcurrentModificationException)。fail-safe 操作副本(CopyOnWriteArrayList 写时复制,ConcurrentHashMap 弱一致性迭代器),不抛异常但可能读到旧数据。fail-fast 只是尽力检测,不能依赖它保证正确性。
初级理解
fail-fast(快速失败):迭代过程中如果集合被结构性修改(add/remove),迭代器立即抛出 ConcurrentModificationException。
fail-safe(安全失败):迭代过程中即使集合被修改,也不会抛异常,因为迭代的是集合的快照(副本)。
| 对比 | fail-fast | fail-safe |
| 代表集合 | ArrayList, HashMap, HashSet | CopyOnWriteArrayList, ConcurrentHashMap |
| 检测机制 | modCount 计数器 | 不检测,操作副本 |
| 异常 | ConcurrentModificationException | 不抛异常 |
| 内存开销 | 无额外开销 | 需要复制集合(内存大) |
| 数据一致性 | 强一致 | 弱一致(可能读到旧数据) |
// fail-fast 示例
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String s : list) {
list.remove(s); // ConcurrentModificationException
}
// fail-safe 示例
List<String> list2 = new CopyOnWriteArrayList<>(Arrays.asList("A", "B", "C"));
for (String s : list2) {
list2.remove(s); // 不抛异常,正常执行
}
中级深入
fail-fast 的实现原理(modCount):集合内部维护一个 modCount 字段,每次结构性修改(add/remove)时 modCount++。迭代器创建时记录 expectedModCount = modCount,每次 next()/remove() 时检查 expectedModCount == modCount,不等则抛异常。
// ArrayList.Itr 的 checkForComodification
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
fail-fast 不能保证一定检测到:modCount 可能溢出回绕,且只在迭代器方法被调用时才检查。所以 fail-fast 只是"尽力而为"的检测机制,不能依赖它来保证正确性。
高级拓展
CopyOnWriteArrayList 的原理:写操作(add/remove/set)时加 ReentrantLock,复制整个数组,在新数组上修改,最后替换引用。读操作不加锁,直接读当前数组。
适用场景:读多写少的场景(如监听器列表、配置信息)。写操作频繁时不适用(每次写都要复制整个数组,开销巨大)。
// CopyOnWriteArrayList 的 add 方法(简化)
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
ConcurrentHashMap 的弱一致性:迭代器不抛 ConcurrentModificationException,遍历的是创建迭代器时的"快照"(但不复制整个 Map),允许遍历过程中其他线程修改。
面试加分项:能说出 CopyOnWriteArrayList 的写时复制原理和适用场景。
实战场景
场景:遍历时删除的正确姿势汇总
// 方式1:Iterator.remove()(所有集合通用)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if ("target".equals(it.next())) it.remove();
}
// 方式2:removeIf(JDK 8+,最简洁)
list.removeIf(s -> "target".equals(s));
// 方式3:倒序遍历(仅 ArrayList 适用)
for (int i = list.size() - 1; i >= 0; i--) {
if ("target".equals(list.get(i))) list.remove(i);
}
// 方式4:收集后批量删除
List<String> toRemove = list.stream()
.filter(s -> "target".equals(s))
.collect(Collectors.toList());
list.removeAll(toRemove);
// 方式5:CopyOnWriteArrayList(并发场景)
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
for (String s : cowList) {
cowList.remove(s); // 不抛异常
}
面试模拟
Q:单线程中也会触发 fail-fast 吗?
A:会。fail-fast 不区分单线程还是多线程,只要迭代过程中结构性修改了集合就会触发。最常见的场景是 for-each 循环中直接调用 list.remove(),这是单线程操作但同样抛异常。
Q:CopyOnWriteArrayList 适合什么场景?为什么不适合写多?
A:适合读多写少(如监听器列表、配置缓存)。因为每次写操作都要 Arrays.copyOf 复制整个数组,写多时内存和 CPU 开销巨大。另外它保证最终一致性而非强一致性——写操作完成后,其他线程的读可能短暂看到旧数据。