fail-fast(快速失败)与 fail-safe(安全失败)

fail-fast

在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。

java.util 中的非线程安全集合的迭代机制都是基于 fail-fast,这里以 ArrayList 为例

原理

在对 ArrayList 的结构进行修改的时候,会修改 modCount 属性,该属性可以理解为 ArrayList 当前结构的版本号

在迭代器创建的时候,会记录 modCount 的值。在迭代的过程中,会判断 modCount 是否被修改,如果 modCount 与原先不一致,则抛出 ConcurrentModificationException 异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private class Itr implements Iterator<E> {
int expectedModCount = modCount;

Itr() {}
// 省略若干代码
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
// 省略若干代码
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

如果要在迭代过程中对集合结构进行修改,尽量使用迭代器中提供的方法。大多数迭代器会提供 remove 方法,有的迭代器还会提供 add 操作的方法,如 ListIterator

需要注意的是,在并发的情况下,并不能保证异常抛出

迭代过程中不能对集合的结构进行修改,如果只是修改某个节点的值,并不算修改

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static void failFast() {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator it = list.iterator();
while (it.hasNext()) {
list.add(4);
System.out.println(it.next());
}
}

//==========================结果==========================
Exception in thread "main" java.util.ConcurrentModificationException
...

Java官方在HashMap 中对快速失败的描述是对集合结构的修改,但修改某个节点的值并不算修改结构,因此下面这段代码不会抛异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static void failFast() {
HashMap<String, String> map = new HashMap<>();
map.put("k1","v1");
map.put("k2","v2");
Iterator it = map.entrySet().iterator();
while (it.hasNext()) {
// 修改 k2 的value,并没有新增节点
map.put("k2","v3");
System.out.println(it.next());
}
}
//==========================结果==========================
k1=v1
k2=v3

fail-safe

在迭代集合的时候,并不是直接遍历集合数据,而是遍历一个副本。当集合被修改时,并不会影响本次迭代。这种迭代方式称为 fail-safe。juc下的安全集合都是fail-safe的实现。以下以CopyOnWriteArrayList 为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static void failSafe() {
List<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> it = list.iterator();
while(it.hasNext()) {
list.add(4);
System.out.println(it.next());
}
}
//==========================结果==========================
1
2
3

COW每次执行 add 操作时,都会修改 array 属性,将新的数组赋值给 array。因此创建迭代器的时候必须保存副本,确保在迭代的时候不会出现问题。