美文网首页Java架构师专题Java开发那些事java集合系列
java集合中常见异常fail-fast(面试常问)

java集合中常见异常fail-fast(面试常问)

作者: 愚公要移山 | 来源:发表于2019-07-05 15:17 被阅读2次

今天来看java集合中一个常见的错误机制fail-fast机制。出现在这个错误机制的本质就是因为单线程和多线程的不同。下面就好好看一下这个机制是怎么是出现的。

一、认识fail-fast

今天在运行项目的时候,突然就出现了ConcurrentModificationException异常。原因是多线程中使用的,因为在多线程中使用了ArrayList,造成了这么一个异常。这是今天所讲的集合的fai-fast机制。

我们先来看看维基百科中的解释:

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

概念:fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。

二、分析fail-fast

为了更好的去了解一下fail-fast,我们先去实现一下这个错误如何产生。

二、分析fail-fast

为了更好的去了解一下fail-fast,我们先去实现一下这个错误如何产生。

异常出现

下面通过一个示例来展示

public class Test {
    private static List<String> list = new ArrayList<String>();
    public static void main(String[] args) {
        //两个线程对同一个ArrayList进行操作
        new ThreadOne().start();
        new ThreadTwo().start();
    }
    private static void printAll() {
        String value = null;
        Iterator iter = list.iterator();
        while(iter.hasNext()) {
            value = (String)iter.next();
            System.out.println(value);
        }
    }
    //线程一:向list中依次添加数据,然后printAll()整个list
    private static class ThreadOne extends Thread {
        public void run() {
            for (int i=0;i<6;i++) {
                list.add(String.valueOf("线程一:java的架构师技术栈"+i));
                printAll();
            }
        }
    }
    //线程二:对ArrayList实现同样的操作
    private static class ThreadTwo extends Thread {
        public void run() {
            for (int i=0;i<6;i++) {
                list.add(String.valueOf("线程二:java的架构师技术栈"+i));
                printAll();
            }
        }
    }
}

​ 看一下在Eclipse中处理的结果:

1-结果.png

现在我们fail-fast实现的方式我们都已经知道了,fail-fast快速失败是在迭代的时候产生的,但是是如何产生的呢?下面我们再来深入的分析一下:

根本原因:

从前面我们知道fail-fast是在操作迭代器时产生的。现在我们来看看ArrayList中迭代器的源代码:

private class IteratorTest implements Iterator<E> {
        int cursor;
        int lastRet = -1;
        int expectedModCount = ArrayList.this.modCount;
        public boolean hasNext() {
            return (this.cursor != ArrayList.this.size);
        }
        public E next() {
            checkForComodification();
        }
        public void remove() {
            if (this.lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
        }
        final void checkForComodification() {
            if (ArrayList.this.modCount == this.expectedModCount)
                return;
            throw new ConcurrentModificationException();
        }
}

从上面的源代码我们可以看出,迭代器在调用next()、remove()方法时都是调用checkForComodification()方法,该方法主要就是检测modCount == expectedModCount ? 若不等则抛出ConcurrentModificationException 异常,从而产生fail-fast机制。到了这一步我们也知道了,想要弄清楚fail-fast机制,首先我们需要搞清楚modCount 和expectedModCount。

​ expectedModCount 是在IteratorTest中定义的:int expectedModCount = ArrayList.this.modCount;所以他的值是不可能会修改的,所以会变的就是modCount。modCount是在 AbstractList 中定义的,为全局变量:

protected transient int modCount = 0; 

​ 那么他什么时候因为什么原因而发生改变呢?请看ArrayList的源码:

    public boolean add(E paramE) {
        ensureCapacityInternal(this.size + 1);
    }

    private void ensureCapacityInternal(int paramInt) {
        if (this.elementData == EMPTY_ELEMENTDATA)
            paramInt = Math.max(10, paramInt);
        ensureExplicitCapacity(paramInt);
    }
    //看到没,是在这里面实现的,修改了modCount
    private void ensureExplicitCapacity(int paramInt) {
        this.modCount += 1;    //修改modCount
    }
   //同理,其他的几个也实现了对modCount的修改
   //remove、
   //fastRemove
   //clear

​ 从上面的源代码我们可以看出,只要是涉及了改变ArrayList元素的个数的方法都会导致modCount的改变。所以我们这里可以初步判断由于expectedModCount 与modCount的改变不同步,导致两者之间不等,从而产生fail-fast机制。

我想各位已经基本了解了fail-fast的机制,那么平常我们如何去规避这种情况呢?这里有两种解决方案:

方案一:在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList(不推荐)

方案二:使用CopyOnWriteArrayList来替换ArrayList。

​ CopyOnWriteArrayList为什么能解决这个问题呢?CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。CopyOnWriteArrayList中add/remove等写方法是需要加锁的,目的是为了避免Copy出N个副本出来,导致并发写。但是。CopyOnWriteArrayList中的读方法是没有加锁的。

我们只需要记住一句话,那就是CopyOnWriteArrayList是线程安全的,所以我们在多线程的环境下面需要去使用这个就可以了。关于CopyOnWriteArrayList更加深入的用法,会在以后的章节中去解释说明。

三、总结

现在我们对fail-fast机制都已经有了了解了。其出现的原因是:当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。类似于我们在学习操作系统的时候出现的问题。
更多教程可关注我的微信公众号:Java的架构师技术栈。回复相应的关键字可获取一系列视频教程,包含Java基础、进阶、架构师等等


微信公众号.png

相关文章

网友评论

    本文标题:java集合中常见异常fail-fast(面试常问)

    本文链接:https://www.haomeiwen.com/subject/ftrehctx.html