Java 中的fail-fast 与 fail-safe

在 Java 中,Set 和 List 可以循环遍历,因为其拥有可迭代能力,主要还是 Iterator 的功劳。

初识

首先看一下二者如何定义:

  • fail-fast 一旦发现遍历的同时有其他人修改,则立即抛出异常
  • fail-safe 当发现在遍历时有其他人修改,就牺牲一致性遍历整个集合

fail-fast

ArrayList 采用的是 fail-fast 机制。我们在遍历过程中,并不能添加或删除元素。

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    list.add(5);
    for(Integer i : list){
        System.out.println(i);
        if(i == 3){
            list.add(4);
        }
    }
    System.out.println(list);
}

当我们在遍历 list 时,添加元素,就会得到异常输出。

1
2
3
Exception in thread “main” java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)

fail-safe

在JDK中,还提供了一个CopyOnWriteArrayList,它就属于 fail-safe.

public static void main(String[] args) {
    List<Integer> list = new CopyOnWriteArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    list.add(5);
    for(Integer i : list){
        System.out.println(i);
        if(i == 3){
            list.add(4);
        }
    }
    System.out.println(list);
}

仅仅将 ArrayList 变为了 CopyOnWriteArrayList,其他代码未改动。

结果能输出,但是丢失了一致性,我们在遍历过程中添加了元素,但是遍历过程中,并未得到该元素。最后一行,我打印了 List 遍历完后的元素,发现 4 已结被添加到列表中了,但是 for 循环并不能感知到该元素。

1
2
3
5
[1, 2, 3, 5, 4]

原因分析

在遍历 List 时,Java 会使用到迭代器 Iterator,而不同的List 实现,对应的迭代器实现不同。

ArrayList

在 ArrayList 实现中,部分关键实现代码如下:

public class ArrayList<E> {

    private class Itr implements Iterator<E> {
        int expectedModCount = modCount;

        public E next() {
            checkForComodification();
        }

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

这里面主要有两个变量很关键:

  • expectedModCount 是 list 原始的元素个数
  • modCount 是每次遍历时,迭代器发现的元素个数

正常情况下,二者是相等的。但是要在遍历过程中调用 add 或者 remove 等修改元素个数的方法,就会使得 modCount 增加或减少,与 expectedModCount 不相等,从而抛出异常 ConcurrentModificationException。

CopyOnWriteArrayList

在 CopyOnWriteArrayList 实现中,部分关键实现代码如下:

public class CopyOnWriteArrayList<E> {

    static final class COWIterator<E> implements ListIterator<E> {
        private final Object[] snapshot;

        private COWIterator(Object[] elements, int initialCursor) {

            snapshot = elements;
        }

        public E next() {
            return (E) snapshot[cursor++];
        }
    }
}

实现思路是通过创建一个临时数组,然后遍历这个临时数组 snapshot 来处理可能出现元素添加的情况。

这也就解释了,在 List 中添加元素,不能在控制台输入添加的元素。因为遍历的是原始 List 的快照,无论在循环中如何修改 List ,都不会受影响。

转载请注明出处:码谱记录 » Java 中的fail-fast 与 fail-safe
标签: