fail-fast机制—高级用法与深入解读

发布于 2022-05-19 02:15

前言

细心地朋友看Java容器源码时一定会发现在list()和listIterator()的注释中都有一句话:

The iterators returned by this class’s iterator and listIterator methods are fail-fast.

我看ArrayList源码没认真想fail-fast是什么意思,看Vector源码时又看到了这个词,而且在翻看Set实现类和Map实现类源码时也看到了这个词。fail-fast是什么?本篇文章以Vector为例来详细解说fail-fast。

what's the "fail-fast"?

下面是Vector中源码的最上部的注释中关于fail-fast的介绍:

原文

The iterators returned by this class's {@link #iterator() iterator} and
 * {@link #listIterator(int) listIterator} methods are <em>fail-fast</em></a>:
 * if the vector is structurally modified at any time after the iterator is
 * created, in any way except through the iterator's own
 * {@link ListIterator#remove() remove} or
 * {@link ListIterator#add(Object) add} methods, the iterator will throw a
 * {@link ConcurrentModificationException}.  Thus, in the face of
 * concurrent modification, the iterator fails quickly and cleanly, rather
 * than risking arbitrary, non-deterministic behavior at an undetermined
 * time in the future.  The {@link Enumeration Enumerations} returned by
 * the {@link #elements() elements} method are <em>not</em> fail-fast.
 *
 * <p>Note that the fail-fast behavior of an iterator cannot be guaranteed
 * as it is, generally speaking, impossible to make any hard guarantees in the
 * presence of unsynchronized concurrent modification.  Fail-fast iterators
 * throw {@code ConcurrentModificationException} on a best-effort basis.
 * Therefore, it would be wrong to write a program that depended on this
 * exception for its correctness:  <i>the fail-fast behavior of iterators
 * should be used only to detect bugs.</i>

翻译

由iterator()和listIterator()返回的迭代器是fail-fast的。在于程序在对list进行迭代时,某个线程对该collection在结构上对其做了修改,这时迭代器就会抛出ConcurrentModificationException异常信息。因此,面对并发的修改,迭代器快速而干净利落地失败,而不是在不确定的情况下冒险。由elements()返回的Enumerations不是fail-fast的。需要注意的是,迭代器的fail-fast并不能得到保证,它不能够保证一定出现该错误。一般来说,fail-fast会尽最大努力抛出ConcurrentModificationException异常。因此,为提高此类操作的正确性而编写一个依赖于此异常的程序是错误的做法,正确做法是:ConcurrentModificationException 应该仅用于检测 bug。

大意为在遍历一个集合时,当集合结构被修改,很有可能会抛出Concurrent Modification Exception。为什么说是很有可能呢?从下文中我们可以知道,迭代器的remove操作(注意是迭代器的remove方法而不是集合的remove方法)修改集合结构就不会导致这个异常。

看到这里我们就明白了,fail-fast 机制是java容器(Collection和Map都存在fail-fast机制)中的一种错误机制。在遍历一个容器对象时,当容器结构被修改,很有可能会抛出ConcurrentModificationException,产生fail-fast。

再理解

大概意思是:在系统设计中,快速失效系统一种可以立即报告任何可能表明故障的情况的系统。快速失效系统通常设计用于停止正常操作,而不是试图继续可能存在缺陷的过程。这种设计通常会在操作中的多个点检查系统的状态,因此可以及早检测到任何故障。快速失败模块的职责是检测错误,然后让系统的下一个最高级别处理错误。

其实就是在做系统设计的时候先考虑异常情况,一旦发生异常,直接停止并上报,比如下面的这个简单的例子:

/**这里的代码是一个对两个整数做除法的方法,
*在fast_fail_method方法中,我们对被除数做了个简单的检查
*如果其值为0,那么就直接抛出一个异常,并明确提示异常原因。
*这其实就是fail-fast理念的实际应用。
*/

public int fast_fail_method(int arg1,int arg2){
    if(arg2 == 0){
        throw new RuntimeException("can't be zero");
    }
    return arg1/arg2;
}

在Java集合类中很多地方都用到了该机制进行设计,一旦使用不当,触发fail-fast机制设计的代码,就会发生非预期情况。我们通常说的Java中的fail-fast机制,默认指的是Java集合的一种错误检测机制。当多个线程对部分集合进行结构上的改变的操作时,有可能会触发该机制时,之后就会抛出并发修改异常ConcurrentModificationException.当然如果不在多线程环境下,如果在foreach遍历的时候使用add/remove方法,也可能会抛出该异常。

什么时候会出现fail-fast?

在以下两种情况下会导致fail-fast,抛出ConcurrentModificationException

单线程环境

遍历一个集合过程中,集合结构被修改。注意,listIterator.remove()方法修改集合结构不会抛出这个异常。

多线程环境

当一个线程遍历集合过程中,而另一个线程对集合结构进行了修改。

单线程环境例子

import java.util.ListIterator;
import java.util.Vector;

public class Test {
/**
     * 单线程测试
     */

    @org.junit.Test
    public void test() 
{
        try {
            // 测试迭代器的remove方法修改集合结构会不会触发checkForComodification异常
            ItrRemoveTest();
            System.out.println("----分割线----");
            // 测试集合的remove方法修改集合结构会不会触发checkForComodification异常
            ListRemoveTest();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 测试迭代器的remove方法修改集合结构会不会触发checkForComodification异常
    private void ItrRemoveTest() {
        Vector list = new Vector<>();
        list.add("1");
        list.add("2");
        list.add("3");
        ListIterator itr = list.listIterator();
        while (itr.hasNext()) {
            System.out.println(itr.next());
            //迭代器的remove方法修改集合结构
            itr.remove();
        }
    }

    // 测试集合的remove方法修改集合结构会不会触发checkForComodification异常
    private void ListRemoveTest() {
        Vector list = new Vector<>();
        list.add("1");
        list.add("2");
        list.add("3");
        ListIterator itr = list.listIterator();
        while (itr.hasNext()) {
            System.out.println(itr.next());
            //集合的remove方法修改集合结构
            list.remove("3");
        }
    }
}

运行结果

1
2
3
----分割线----
1
java.util.ConcurrentModificationException
    at java.util.Vector$Itr.checkForComodification(Unknown Source)

从结果中可以看到迭代器itr的remove操作并没有出现ConcurrentModificationException异常。而集合的remove操作则产生了异常。

多线程环境例子

import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Vector;

public class Test {
    private static List<String> list = new Vector<String>();

    /**
     * 多线程情况测试
     */

    @org.junit.Test
    public void test2() 
{
        list.add("1");
        list.add("2");
        list.add("3");
        // 同时启动两个线程对list进行操作!
        new ErgodicThread().start();
        new ModifyThread().start();
    }

    /**
     * 遍历集合的线程
     */

    private static class ErgodicThread extends Thread {
        public void run() {
            int i = 0;
            while (i < 10) {
                printAll();
                i++;
            }
        }
    }

    /**
     * 修改集合的线程
     */

    private static class ModifyThread extends Thread {
        public void run() {
            list.add(String.valueOf("5"));
        }
    }
    /**
     * 遍历集合
     */

    private static void printAll() {
        Iterator iter = list.iterator();
        while (iter.hasNext()) {
            System.out.print((String) iter.next() + ", ");
        }
        System.out.println();
    }
}

运行结果

123
123
123
1, Exception in thread "Thread-0" java.util.ConcurrentModificationException
    at java.util.Vector$Itr.checkForComodification(Unknown Source)

从结果中可以看出当一个线程遍历集合,而另一个线程对这个集合的结构进行了修改,确实有可能触发ConcurrentModificationException异常。

fail-fast实现原理

下面是Vector中迭代器Itr的部分源码

/**
 * An optimized version of AbstractList.Itr
 */

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

    //省略的部分代码

    public void remove() {
        if (lastRet == -1)
            throw new IllegalStateException();
        synchronized (Vector.this) {
            checkForComodification();
            Vector.this.remove(lastRet);
            expectedModCount = modCount;
        }
        cursor = lastRet;
        lastRet = -1;
    }

    @Override
    public void forEachRemaining(Consumer<? super E> action) {
            //省略的部分代码
            checkForComodification();
        }
    }

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

从代码中可以看到,每次初始化一个迭代器都会执行int expectedModCount = modCount;。modcount意为moderate count,即修改次数,对集合内容的修改都将增大这个值,如modCount++;。在迭代器初始化过程中会执行int expectedModCount = modCount;来记录迭会通过checkForComodification()方法判断modCount和expectedModCount 是否相等,如果不相等就表示已经有线程修改了集合结构。

使用迭代器的remove()方法修改集合结构不会触发ConcurrentModificationException,现在可以在源码中看出来是为什么。在remove()方法的最后会执行expectedModCount = modCount;,这样itr.remove操作后modCount和expectedModCount依然相等,就不会触发ConcurrentModificationException了。

如何避免fail-fast?

使用java.util.concurrent包下的类去取代java.util包下的类。所以,本例中只需要将Vector替换成java.util.concurrent包下对应的类即可。

import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Vector;
import java.util.concurrent.CopyOnWriteArrayList;

public class Test 
    /**
     * CopyOnWriteArrayList的fail-fast测试
     */

    @org.junit.Test
    public void test3() 
{
        try {
            List list = new CopyOnWriteArrayList<>();
            list.add("1");
            list.add("2");
            list.add("3");
            ListIterator itr = list.listIterator();
            while (itr.hasNext()) {
                System.out.println(itr.next());
                list.add("5");
                list.remove("2");
            }
            System.out.println(list.toString());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果

1
2
3
[13555]

从运行结果中不难发现,在遍历过程中,使用集合的remove()方法修改集合结构并没有产生ConcurrentModificationException。


本文来自网络或网友投稿,如有侵犯您的权益,请发邮件至:aisoutu@outlook.com 我们将第一时间删除。

相关素材