• java集合系列(4)fail-fast机制(面试常问)


    此文章转载于Java的架构师技术栈微信公众号

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

    一、认识fail-fast

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

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

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

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

    二、分析fail-fast

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

    异常出现

    下面通过一个示例来展示

    public class Test {
        private static List<String> list = new ArrayList<>();
        public static void main(String[] args) {
            //两个线程对同一个ArrayList进行操作
            new ThreadOne().start();
            new ThreadTwo().start();
        }
        //遍历list中的所有元素并输出
        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(("线程一:java的架构师技术栈"+i));
                    printAll();
                }
            }
        }
        //线程二:对ArrayList实现同样的操作
        private static class ThreadTwo extends Thread {
            public void run() {
                for (int i=0;i<6;i++) {
                    list.add(("线程二:java的架构师技术栈"+i));
                    printAll();
                }
            }
        }
    }
     

       看一下处理的结果:

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

    根本原因:

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

    private class Itr implements Iterator<E> {
            int cursor;       // index of next element to return
            int lastRet = -1; // index of last element returned; -1 if no such
            int expectedModCount = modCount;//注意这里
    
            Itr() {}
    
            public boolean hasNext() {
                return cursor != size;
            }
    
            @SuppressWarnings("unchecked")
            public E next() {
                checkForComodification();
                int i = cursor;
                if (i >= size)
                    throw new NoSuchElementException();
                Object[] elementData = ArrayList.this.elementData;
                if (i >= elementData.length)
                    throw new ConcurrentModificationException();
                cursor = i + 1;
                return (E) elementData[lastRet = i];
            }
    
            public void remove() {
                if (lastRet < 0)
                    throw new IllegalStateException();
                checkForComodification();
    
                try {
                    ArrayList.this.remove(lastRet);
                    cursor = lastRet;
                    lastRet = -1;
                    expectedModCount = modCount;
                } catch (IndexOutOfBoundsException ex) {
                    throw new ConcurrentModificationException();
                }
            }
    
            @Override
            @SuppressWarnings("unchecked")
            public void forEachRemaining(Consumer<? super E> consumer) {
                Objects.requireNonNull(consumer);
                final int size = ArrayList.this.size;
                int i = cursor;
                if (i >= size) {
                    return;
                }
                final Object[] elementData = ArrayList.this.elementData;
                if (i >= elementData.length) {
                    throw new ConcurrentModificationException();
                }
                while (i != size && modCount == expectedModCount) {
                    consumer.accept((E) elementData[i++]);
                }
                // update once at end of iteration to reduce heap write traffic
                cursor = i;
                lastRet = i - 1;
                checkForComodification();
            }
    
            final void checkForComodification() {
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
            }
        }
     

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

           expectedModCount 是在IteratorTest中定义的:

    int expectedModCount = modCount;

    所以他的值是不可能会修改的,所以会变的就是modCount。modCount是在 AbstractList 中定义的,为全局变量:

    protected transient int modCount = 0;

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

    
    
    public E remove(int index) {
    rangeCheck(index);

    modCount++;//这里改变了
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
    System.arraycopy(elementData, index+1, elementData, index,
    numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
    }
    
    
     
      //同理,其他的几个也实现了对modCount的修改
      //add、
      //fastRemove
      //clear

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

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

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

           方案二:使用juc下面的CopyOnWriteArrayList来替换ArrayList。

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

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

    三、总结

    现在我们对fail-fast机制都已经有了了解了。其出现的原因是:当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。类似于我们在学习操作系统的时候出现的问题。  

  • 相关阅读:
    搭建中文分词工具——递归方法
    (五)django上传文件并读取相应数据存入数据库
    (四)django上传文件并读取存入数据库
    Django中的外键赋值
    (二)Django连接本地mysql异常
    (一)环境搭建——Django
    论文阅读笔记:《Interconnected Question Generation with Coreference Alignment and Conversion Flow Modeling》
    AWS EC2 CentOS release 6.5 部署redis
    2016年简直一晃而过
    Android开发学习之路--性能优化之布局优化
  • 原文地址:https://www.cnblogs.com/lusaisai/p/12386635.html
Copyright © 2020-2023  润新知