你是否遇到过这样的异常?
java.util.ConcurrentModificationException
这其实是 Java 集合“自我保护”的一种机制 —— Fail-Fast。但在某些场景下我们又会发现某些集合在并发环境下并不会抛异常,这是为什么?本篇为你详解 Java 集合框架中 Fail-Fast 与 Fail-Safe 的设计原理与实现机制。
一、什么是 Fail-Fast 与 Fail-Safe?
✅ Fail-Fast(快速失败)
当多个线程对同一个集合进行结构性修改(如 add/remove),一旦检测到非当前线程修改,立即抛出异常,避免数据不一致。
常见集合:
-
ArrayList
-
HashSet
-
HashMap
-
LinkedList
✅ Fail-Safe(安全失败)
这种机制不会抛出异常,它会在遍历期间创建一个副本,从副本中读取数据,即便原集合被修改也不会出错。
常见集合:
-
CopyOnWriteArrayList
-
ConcurrentHashMap
二、Fail-Fast 的实现原理解析
核心字段:modCount(修改次数)
📦 示例(基于 ArrayList):
public class ArrayList<E> {
protected transient int modCount = 0;
public E remove(int index) {
modCount++; // 每次结构性修改都会+1
...
}
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
int expectedModCount = modCount;
public E next() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
...
}
}
}
🧠 设计目的:
-
防止脏读:遍历过程中检测集合是否被其他线程修改;
-
性能优先:不加锁,只在问题发生时终止遍历,符合“快速失败”思想;
三、Fail-Safe 的底层机制剖析
💡 CopyOnWriteArrayList 的思路:
写操作发生时,复制原数组,修改副本,最后替换引用,读操作不加锁。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
Object[] newElements = Arrays.copyOf(elements, elements.length + 1);
newElements[elements.length] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
-
避免并发修改影响迭代;
-
适用于 读多写少 场景;
-
写时开销大,不适合频繁修改的集合场景;
💡 ConcurrentHashMap 的思路:
-
使用 Segment 锁或 CAS + volatile 保证并发安全;
-
keySet().iterator() 返回的是弱一致性的迭代器;
-
遍历期间允许并发修改,不会抛出异常;
四、实际开发中该如何选用?
场景 | 推荐集合 | 机制 | 特点 |
---|---|---|---|
读多写少,迭代期间避免并发异常 | CopyOnWriteArrayList | Fail-Safe | 写开销大,读性能好 |
高并发读写 + Map结构 | ConcurrentHashMap | Fail-Safe | 支持高并发,弱一致性 |
单线程或明确同步控制下 | ArrayList、HashMap | Fail-Fast | 性能好,需避免并发修改 |
保证一致性,宁可失败 | LinkedList、HashSet | Fail-Fast | 适合用于数据结构修改频繁场景下的保护 |
五、常见面试问题与误区
1.为什么 Fail-Fast 不使用锁来避免并发问题?
答:Fail-Fast 是一种检测机制,不代表是线程安全的设计。使用锁反而会带来性能下降,而 Fail-Fast 的本意就是“不安全就立即报错”。
2.为什么 ConcurrentHashMap 的迭代器不抛异常?
答:它使用 弱一致性(Weakly Consistent)迭代器,即迭代期间的数据不一定是最新的,但不会抛异常。
3.CopyOnWriteArrayList 的 iterator 有什么性能隐患?
答:迭代的是快照,如果集合很大,复制开销会非常高;此外,如果频繁写入,会产生大量短生命周期的数组对象,GC 压力变大。
六、如何用 Iterator.remove()安全修改集合?
🚫 错误写法:使用 for-each直接删除元素
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
for (String item : list) {
if ("A".equals(item)) {
list.remove(item); // 抛 ConcurrentModificationException
}
}
✅ 正确写法:使用 Iterator.remove()
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("A".equals(item)) {
iterator.remove(); // 安全地删除元素
}
}
🔍 原理解析:
-
Iterator 内部会在调用 next() 后标记当前位置;
-
remove() 会将该位置的元素移除,并更新 modCount 与 expectedModCount;
-
如果使用 list.remove() 则破坏了预期修改计数的一致性,从而触发 fail-fast 机制。
七、for-each与 Iterator的底层行为对比?
🌟 语法糖剖析:
for (String item : list) {
// 实际是下面的写法:
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
...
}
}
❗误区警告:
-
虽然 for-each 使用的是 Iterator,但你无法手动调用 remove(),因此不适合用于迭代删除场景;
-
若在 for-each 中结构性修改集合(如 add/remove),仍然会抛 ConcurrentModificationException。
✅ 建议:
-
遍历过程中需要修改集合,始终使用显式 Iterator;
-
如果使用 Stream 处理,请确保不在 map/filter/forEach 中修改原集合。
八、自定义集合如何支持 Fail-Fast 检测机制?
当我们实现一个自定义集合类时,如果希望其支持 Fail-Fast 行为,需要模仿 JDK 的方式,维护一个 modCount 修改次数字段。
🛠 示例:自定义集合类
public class MyList<E> extends AbstractList<E> {
private List<E> internal = new ArrayList<>();
protected transient int modCount = 0;
@Override
public E get(int index) {
return internal.get(index);
}
@Override
public int size() {
return internal.size();
}
@Override
public E remove(int index) {
modCount++; // 修改次数+1
return internal.remove(index);
}
@Override
public Iterator<E> iterator() {
return new MyIterator();
}
private class MyIterator implements Iterator<E> {
private int cursor = 0;
private int expectedModCount = modCount;
@Override
public boolean hasNext() {
return cursor < size();
}
@Override
public E next() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
return internal.get(cursor++);
}
}
}
✅ 核心实现要点:
-
每次结构性修改时增加 modCount;
-
迭代器内部缓存初始 modCount 到 expectedModCount;
-
遍历过程中对比两者是否一致,若不一致则抛异常;
九、总结
-
Java 集合中的 Fail-Fast 和 Fail-Safe 是对并发修改行为的两种不同策略;
-
Fail-Fast 不是线程安全机制,而是一种错误检测机制;
-
想要安全遍历集合,必须使用 Fail-Safe 容器或同步控制;