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

2022年05月15日 阅读数:4
这篇文章主要向大家介绍fail-fast机制—高级用法与深刻解读,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。


前言

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

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

我看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方法)修改集合结构就不会致使这个异常。ide

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

再理解

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

其实就是在作系统设计的时候先考虑异常状况,一旦发生异常,直接中止并上报,好比下面的这个简单的例子:测试

/**这里的代码是一个对两个整数作除法的方法,
*在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方法,也可能会抛出该异常。ui

何时会出现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();
    }
}

运行结果

1, 2, 3, 
1, 2, 3, 
1, 2, 3, 
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
[1, 3, 5, 5, 5]

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


推荐阅读Java小白进阶架构师学习路线