zoukankan      html  css  js  c++  java
  • Java容器中的快速失败机制(fail-fast)

    一、快速报错机制(fail-fast)

    这是《Java编程思想》中关于快速报错机制的描述

    Java容器有一种保护机制,能够防止多个进程同时修改同一个容器的内容。如果在你迭代遍历容器的过程中,另一个进程介入其中,并且插入、删除或者修改此容器内的某个对象,那么就会出现问题:也许迭代过程中已经处理过容器中的该元素了,也许还没处理,也许在调用size()之后容器的尺寸收缩了——还有许多灾难情景。Java容器类类库采用快速报错(fail-fast)机制。它会探查容器上的任何除了你的进程所进行的操作以外的所有变化,一旦它发现其它进程修改了容器,就会立刻抛出ConcurrentModificationException异常。这就是“快速报错”的意思——即,不是使用复杂的算法在事后来检查问题。

    ——from《Java编程思想》p517

    二、ArrayList中的快速报错机制分析

    快速报错机制在容器中使用非常广泛,我们最常用的ArrayList就用到了快速报错机制。下面是ArrayList的迭代器源码。

        #ArrayList的Iterator源码分析
        
        public Iterator<E> iterator() {
            return new Itr();
        }
    
        /**
         * An optimized version of AbstractList.Itr
         * 
         * 覆盖了父类中AbstractList.Itr的实现(优化版)
         */
        private class Itr implements Iterator<E> {
            //下一个要返回元素的索引
            int cursor;       // index of next element to return
            //最后一个要返回元素的索引,-1表示不存在
            int lastRet = -1; // index of last element returned; -1 if no such
            //记录期望的修改次数(用于保证迭代器在遍历过程中不会有对集合的修改操作(迭代器的自身的remove方法除外))
            int expectedModCount = modCount;
    
            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();
                }
            }
    
            /**
             * 检查修改次数
             */
            final void checkForComodification() {
                //实际的修改次数和期望的修改次数不匹配,则抛出并发修改异常
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
            }
        }

    原来,ArrayList从其父类AbstractList继承了一个modCount属性,每当对ArrayList进行修改(add,remove,clear等)时,就会相应的modCount的值加1。而ArrayList中迭代器的实现类Itr也有一个expectedModCount属性。一旦调用iterator()方法来使用迭代器时,Itr类也就被初始化,expectedModCount就会被赋予一个与modCount相等的值。如果接下来进行遍历操作,则每次调用next()方法获取值时都会先进行修改检查(checkForComodification),也就是检查modCount和expectedModCount两个值是否相等。如果在遍历过程中进行了对集合的其它修改操作而使得modCount值发生变化,从而造成两者不等,就立即抛出ConcurrentModificationException。这不就是乐观锁的思想吗。这就是快速失败机制(fail-fast)的实现原理。

    另外,我们注意到迭代器自身也提供了remove方法,但该方法并不会修改modCount的值,这是因为我们通常也会通过迭代遍历去删除某一个指定的元素,所以迭代器中自身提供了该remove方法,并保证该remove方法是安全的,而不希望我们在迭代时使用容器提供remove方法。

    我们可以得出这样的结论:
    一旦使用了迭代器,无论是否已经开始迭代,都不能在接下来的过程对容器进行修改操作,这里的修改指的是容器自身提供的add/remove等修改方法。但是可以使用迭代器自身提供的修改方法(通常只有remove)。

    三、避免依赖快速报错机制

    需要说明两点
    ①.虽然ConcurrentModificationException被译为并发修改异常,但这里的"并发",并非仅仅指的是多线程场景。
    在单线程情况下:要确保Iterator遍历过程顺利完成,必须保证遍历过程中不更改集合的内容(Iterator的remove()方法除外)。

    多线程情况下:如果要在多线程环境中,在迭代ArrayList的同时也要修改ArrayList,则可以使用
    Collections.synchronizedList(List list)或者CopyOnWriteArrayList。其中CopyOnWriteArrayList是可以避免ConcurrentModificationException。

    实际上CopyOnWriteArrayList、ConcurrentHashMap和CopyOnWriteArraySet都使用了可以避免ConcurrentModificationException的技术。

    ②.迭代器的快速失败机制无法得到保证,它不能保证一定发生,只是会尽最大努力抛出ConcurrentModificationException异常。

    为什么不能保证一定发生呢?
    其实原因很简单,再回到前面ArrayList的迭代器代码,我们注意到修改检查并非在同步下进行的,如果容器进行修改操作而导致modCount发生变化,由于可见性,迭代器可能会看到失效的modCount值,从而不会意识到已经发生修改。而这是一种设计上的权衡。

    因此,为提高此类操作的正确性,我们不能依赖于该机制,而要使用上一条中提到的线程安全的容器。

    四、CopyOnWriteArrayList不支持快速报错机制

    ArrayList在迭代遍历的同时进行并发修改是会发生快速失败,因此需要使用同步保证迭代的安全性。但ArrayList对应的线程安全容器CopyOnWriteArrayList能在遍历的同时进行修改,而且未使用同步。

    那么它是怎么实现的呢?实际上很简单,CopyOnWriteArrayList每次在进行修改操作时,都会新生成一个数组,然后在新数组上修改,然后替换原数组,而遍历操作则是直接操作原有数组。因为遍历和修改操作的目标都不一样,因此根本不会互相影响。

    来看看源码。

        /**
         * 返回迭代器
         * 
         * 返回的迭代器提供了该迭代器被创建时列表的快照。
         * 当移动迭代器时,不需要同步。
         * 迭代器不支持remove方法
         */
        public Iterator<E> iterator() {
            return new COWIterator<E>(getArray(), 0);
        }
        
        /**
         * 内部迭代器的实现类
         */
        private static class COWIterator<E> implements ListIterator<E> {
            /**数组的快照*/
            private final Object[] snapshot;
            private int cursor;
    
            private COWIterator(Object[] elements, int initialCursor) {
                cursor = initialCursor;
                snapshot = elements;
            }
    
            public boolean hasNext() {
                return cursor < snapshot.length;
            }
    
            public boolean hasPrevious() {
                return cursor > 0;
            }
    
            @SuppressWarnings("unchecked")
            public E next() {
                if (! hasNext())
                    throw new NoSuchElementException();
                //在快照数组上进行迭代操作
                return (E) snapshot[cursor++];
            }
    
            @SuppressWarnings("unchecked")
            public E previous() {
                if (! hasPrevious())
                    throw new NoSuchElementException();
                return (E) snapshot[--cursor];
            }
    
            public int nextIndex() {
                return cursor;
            }
    
            public int previousIndex() {
                return cursor-1;
            }
    
            /**
             * 迭代器不支持remove/set/add方法。
             * 总是抛出UnsupportedOperationException异常
             */
            public void remove() {
                throw new UnsupportedOperationException();
            }
            public void set(E e) {
                throw new UnsupportedOperationException();
            }
            public void add(E e) {
                throw new UnsupportedOperationException();
            }
        }

    迭代器只用于迭代,不提供任何修改操作。这点恰好跟ArrayList相反。

    public class Demo {
    
        public static void main(String[] args) {
            CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
            //添加元素0-4
            for(int i=0;i<5;i++){
                list.add(i+"");
            }
            System.out.println(list);//[0, 1, 2, 3, 4]
            
            //进行迭代
            Iterator iterator = list.iterator();
            while(iterator.hasNext()){
                String num = (String) iterator.next();
                //迭代时,删除3
                if("3".equals(num)){
                    //iterator.remove();//iterator不支持修改方法(add,set,remove)
                    list.remove(num);//使用原容器的remove方法
                };
            }
            System.out.println(list);//[0, 1, 2, 4]
        }
    }

    五、练习(如何安全的使用迭代器,如何正确的删除容器中的元素)

    程序功能:分别使用for,foreach,iterator来遍历(迭代)容器,然后删除其中的值为"傻强"这个元素。

    public class TestTest {
        
        private List<String> list;
        
        /**
         * 初始化操作
         */
        @Before
        public void setUp(){
            list = new ArrayList<String>();
            list.add("刘德华");        
            list.add("周润发");
            list.add("傻强");
            list.add("古天乐");
            list.add("刘青云");
            System.out.println(list);
        }
        
        /**
         * Demo1:使用for循环,删除元素
         */
        @Test
        public void testFor(){            
            for(int i=0;i<list.size();i++){
                //删除傻强
                if("傻强".equals(list.get(i))){
                    list.remove(i);
                }
            }
            System.out.println(list);
        }
        
        /**
         * Demo2:使用foreach,删除元素【错误】
         */
        @Test
        public void testForeach(){        
            for (String s : list) {
                //删除傻强
                if("傻强".equals(s)){
                    list.remove(s);//使用容器自身的remove
                }
            }
            System.out.println(list);
        }
        
        /**
         * Demo3:使用Iterator,调用Iterator的remove()删除元素
         */
        @Test
        public void testIterator(){
            Iterator<String> iterator = list.iterator();
            while(iterator.hasNext()){
                String s= iterator.next();
                //删除lisi
                if("傻强".equals(s)){
                    iterator.remove();//使用迭代器的remove
                }
            }
            System.out.println(list);
        }
        
        /**
         * Demo4:使用Iterator,调用集合自身的remove()删除元素【错误】
         */
        @Test
        public void testIterator2(){
            Iterator<String> iterator = list.iterator();
            while(iterator.hasNext()){
                String s= iterator.next();
                //删除傻强
                if("傻强".equals(s)){
                    list.remove(s);//使用容器的remove
                }
            }
            System.out.println(list);
        }
        
        /**
         * Demo5:获得iterator后进行了错误操作【错误】
         */
        @Test
        public void testIterator3(){
            Iterator<String> iterator = list.iterator();
            //这是错误的操作。获取迭代器后不能再调用容器的修改方法
            list.add("这是错误的行为");
            //这是允许的。
            //iterator.remove(xx);
            
            while(iterator.hasNext()){
                String s= iterator.next();
                //删除傻强
                if("傻强".equals(s)){
                    iterator.remove();//使用迭代器的remove
                }
            }
            System.out.println(list);
        }
    }

    结果:除了demo1和demo3正确,其它demo都会报ConcurrentModificationException异常。

    面试题
    1.什么是快速失败机制(fail-fast)?

    2.ArrayList中快速失败机制的实现原理?

    3.为什么不能依赖快速失败机制?
    4.如何正确的删除容器中的元素?

  • 相关阅读:
    微信开发返回验证来源方式代码
    Yii 开发过程 tips
    PHP 搜索分词实现代码
    PHP 中文字符串截取
    Ubuntu16.04设置静态ip
    Linux(Ubuntu18.04)安装Chrome浏览器
    ubuntu18.04安装redis
    ubuntu18.04虚拟机安装docker
    虚拟机安装ssh,关闭防火墙
    面试送命题,你为什么从上家公司离职?(面试题总结大全)
  • 原文地址:https://www.cnblogs.com/rouqinglangzi/p/10310735.html
Copyright © 2011-2022 走看看