Java容器系列-Fail-Fast机制到底是什么

fail-fast 实际上是一种系统设计的方法,维基百科上是这样解释的:

在系统设计中,一个 fail-fast 的系统可以通过特定的接口快速报告系统中任何潜在的故障。fail-fast 系统在发现故障时不会尝试继续运行系统,而会立即停止当前的操作。在进行一个操作时,会在多个检查点检查系统的状态,所以出现故障可以被尽早发现。fail-fast 模块的职责就是检查是否有故障,发现故障后会尽快通知上层系统来处理故障。

上面的文字看起来有点晦涩,实际的意思就是 fail-fast 是一种快速发现系统故障的机制,在检测到系统状态不对时,会立即停止当前的操作,让上层的系统来处理这些故障。

与 fail-fast 相对的是 fail-safe。顾名思义,fail-safe 在故障发生之后会维持系统继续运行。Java 在容器中用到了这两种机制。

当使用迭代器(iterator)遍历容器时,迭代器分为两种情况,一种是 fail-fast,另一种是 fail-sale。

fail-fast 在遍历时,如果容器的元素被修改,就会报 ConcurrentModificationException 异常,然后终止遍历。

fail-safe 意味着在遍历元素时,即使容器的元素被修改,不会抛出异常,也不会停止遍历。

本文基于 JDK1.8

fail-fast 具体实现

看如下的代码:

1
2
3
4
5
6
7
8
9
ArrayList<Integer> integers = new ArrayList<>();
integers.add(1);
integers.add(2);
integers.add(3);
Iterator<Integer> itr = integers.iterator();
while (itr.hasNext()) {
Integer a = itr.next();
integers.remove(a);
}

上面使用 ArrayList 的代码会报 ConcurrentModificationException 异常。

1
2
3
4
5
6
7
8
9
List<Integer> integers = new CopyOnWriteArrayList<>();
integers.add(1);
integers.add(2);
integers.add(3);
Iterator<Integer> itr = integers.iterator();
while (itr.hasNext()) {
Integer a = itr.next();
integers.remove(a);
}

而使用 CopyOnWriteArrayList 的代码则不会报异常。

fail-fast 机制和 modCount 这个变量有关,这个变量会记录容器被修改的次数,可以理解为容器对象的版本号。

那么容器怎样才算是被修改呢?

  • 当容器元素被删除
  • 当容器增加一个元素
  • 当容器中的元素执行了排序操作
  • 当容器被其他容器对象替代

需要注意,修改容器中元素的内容 modCount 不会增加

当容器使用迭代器对元素进行迭代时,会把 modCount 赋值给 expectedModCount。

1
int expectedModCount = modCount;

在迭代的过程中,会不断的去检查 expectedModCount 与 modCount 的值是否相等,如果不相等,就说明在容器迭代的过程中,有其他的操作修改了容器,导致 modCount 的值增加,那么就会报 ConcurrentModificationException 异常。

注:fail-fast 机制不仅仅在迭代器中使用了,容器的增删改查和序列化等操作中也用到了。

有没有办法在迭代的过程中删除某些元素?使用迭代器本身的 remove 方法就行,这样不会产生异常:

1
2
3
4
5
6
Iterator<Integer> itr = integers.iterator();
while (itr.hasNext()) {
if (itr.next() == 2) {
itr.remove();
}
}

不会产生异常的原因是在删除元素后,把最新的 modCount 的值赋值给了 expectedModCount,代码如下:

1
2
3
4
5
// ListItr.remove() method
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;

fail-fast 机制可以用来检测容器元素是否被修改,但是需要注意的是,不能依赖 fail-fast 机制来保证容器的元素不被修改,也就是说,不要在没有同步的情况下并发的修改容器中的元素。fast-fast 机制本来的职责就是检测系统的错误,所以仅仅只用它来检测 bug,而不要作其他的用途。

注:同步是并发编程中的一个术语,如果说一段代码是同步的,那就代表是线程安全的

fail-safe 具体实现

和 fail-fast 不同的是,使用了 fail-safe 的容器则可以在迭代的过程中任意的修改容器的元素而不会报错。本质是因为迭代的是容器元素的副本,也就是说是将容器的元素拷贝了一份再进行遍历,这样即使原容器被修改,也不会影响到当前正在遍历的元素。

CopyOnWriteArrayList 是一个支持 fail-safe 的容器,它获取迭代器的代码如下:

1
2
3
4
5
6
// CopyOnWriteArrayList.listIterator() method
Object[] es = getArray();
int len = es.length;
if (index < 0 || index > len)
throw new IndexOutOfBoundsException(outOfBounds(index, len));
return new COWIterator<E>(es, index);

1
2
3
4
5
6
7
8
// COWIterator inner class
private final Object[] snapshot;
private int cursor;

COWIterator(Object[] es, int initialCursor) {
cursor = initialCursor;
snapshot = es;
}

COWIterator 将容器的元素做了一个快照,后续的操作都是在这个快照上进行的。

为了支持 fail-safe 特性,需要付出额外的代价:

  • 迭代器中的元素不是容器的最新状态
  • 需要额外的内存或者时间上的开销

java.util 包下的容器都有 fail-fast 机制,而java.util.concurrent 包下的容器则都是 fail-safe 的。

REF:

  • https://medium.com/@mr.anmolsehgal/fail-fast-and-fail-safe-iterations-in-java-collections-11ce8ca4180e
  • https://en.wikipedia.org/wiki/Fail-fast
  • https://en.wikipedia.org/wiki/Fail-safe
  • JDK document

微信公众号

© 2018 ray