fail-fast(快速失败/报错机制)-ConcurrentModificationException

来源:互联网 发布:淘宝商城怎么开 编辑:程序博客网 时间:2024/05/16 18:23

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

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

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

下面以一个demo开始,来理解快速报错机制。

二、遍历容器的几种方式

程序功能:分别使用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);            }        }        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("这是错误的行为");        while(iterator.hasNext()){            String s= iterator.next();            //删除傻强            if("傻强".equals(s)){                iterator.remove();            }        }        System.out.println(list);    }}

结果:只有Demo1和Demo3正确。其它demo都会报ConcurrentModificationException异常。这里就用到了fail-fast机制。

三、ArrayList中Iterator源码分析

接下来开始分析。我们先看看ArrayLis中的关于迭代器的代码

    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的值。
而ArrayList中迭代器的实现类Itr也有一个expectedModCount属性,一旦使用迭代器遍历容器时,就要调用iterator()方法,Itr类也就被初始化,expectedModCount就会被赋予一个与modCount相等的值。
接下来在遍历过程中,每次调用next()方法获取值时都会检查modCount和expectedModCount两个值是否相等(checkForComodification)。如果在遍历过程中出现了对集合的其它修改操作,从而造成两者不等,就会抛出ConcurrentModificationException。这不就是乐观锁的实现思想吗。

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

四、避免fail-fast

要说明两点
1.虽然ConcurrentModificationException被译为并发修改异常,但这里的”并发”,并非仅仅指的是多线程场景,前面的例子很显然是单线程场景。
在单线程情况下

要确保Iterator遍历过程顺利完成,必须保证遍历过程中不更改集合的内容(Iterator的remove()方法除外)。

多线程情况下

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

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

2.迭代器的快速失败行为无法得到保证,它不能保证一定会出现该错误,但是快速失败操作会尽最大努力抛出ConcurrentModificationException异常。因此,为提高此类操作的正确性,我们不能依赖于此异常,而要使用上一条中提到的线程安全的容器。

五、CopyOnWriteArrayList不使用fail-fast机制

通过上面的分析,我么知道了ArrayList一边使用迭代器遍历一边修改是会发生ConcurrentModificationException。但是,ArrayList对应的线程安全容器CopyOnWriteArrayList却不会发生ConcurrentModificationException。那是为什么呢?先来看源码。

    /**     * 返回迭代器     *      * 返回的迭代器提供了该迭代器被创建时列表的快照。     * 当移动迭代器时,不需要同步。     * 迭代器不支持remove方法     */    public Iterator<E> iterator() {        return new COWIterator<E>(getArray(), 0);    }    /**     * 内部迭代器的实现类     */    private static class COWIterator<E> implements ListIterator<E> {        /**数组的快照*/        private final Object[] snapshot;        /** Index of element to be returned by subsequent call to next.  */        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;        }        /**         * Not supported. Always throws UnsupportedOperationException.          * remove is not supported by this iterator.         * 不支持。总是抛出不支持的操作异常         * 迭代器不支持remove方法。         */        public void remove() {            throw new UnsupportedOperationException();        }        /**         * Not supported. Always throws UnsupportedOperationException.         * set is not supported by this iterator.         * 不支持。总是抛出不支持的操作异常         * 迭代器不支持set方法。         *          */        public void set(E e) {            throw new UnsupportedOperationException();        }         /**         * Not supported. Always throws UnsupportedOperationException.         * add is not supported by this iterator.         * 不支持。总是抛出不支持的操作异常         * 迭代器不支持aa方法。         *          */        public void add(E e) {            throw new UnsupportedOperationException();        }    }

原来,CopyOnWriteArrayList中的迭代器在创建之初,会保存一份对原数组的快照,之后所有迭代器的操作都是在快照数组上进行的,原数组一点影响都没有。同时,即使是对快照数组进行操作,也根本不支持迭代器上的修改(add,set,remove)操作。因此,根本就不会发生ConcurrentModificationException。

虽然在迭代时不支持迭代器上的修改操作,但是仍然可以直接使用容器的修改方法,这点恰好跟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]    }}
0 0
原创粉丝点击