java.util.ConcurrentModification

作者: MrDTree | 来源:发表于2016-02-16 16:47 被阅读13069次

    本想翻译一下java.util.ConcurrentModificationException这篇文章的。但发现讲的不够详细深入,查了一些资料后决定自己扩展一下。水平有限,仅仅作为一个学习总结啦。

    异常产生

    当我们迭代一个ArrayList或者HashMap时,如果尝试对集合做一些修改操作(例如删除元素),可能会抛出java.util.ConcurrentModificationException的异常。

    import java.util.Iterator;
    import java.util.List;
    
    public class AddRemoveListElement {
     
        public static void main(String args[]) {
            List<String> list = new ArrayList<String>();
            list.add("A");
            list.add("B");
     
            for (String s : list) {
                if (s.equals("B")) {
                    list.remove(s);
                }
            }
            
            //foreach循环等效于迭代器
            /*Iterator<String> iterator=list.iterator();
            while(iterator.hasNext()){
                String s=iterator.next();
                if (s.equals("B")) {
                    list.remove(s);
                }
            }*/    
        }
    }
    

    Output:


    异常原因

    ArrayList的父类AbstarctList中有一个域modCount,每次对集合进行修改(增添元素,删除元素……)时都会modCount++

    而foreach的背后实现原理其实就是Iterator(关于Iterator可以看Java Design Pattern: Iterator),等同于注释部分代码。在这里,迭代ArrayList的Iterator中有一个变量expectedModCount,该变量会初始化和modCount相等,但如果接下来如果集合进行修改modCount改变,就会造成expectedModCount!=modCount,此时就会抛出java.util.ConcurrentModificationException异常

    过程如下图:


    绘图2.jpg

    我们再来根据源码详细的走一遍这个过程

    /*
     *AbstarctList的内部类,用于迭代
     */
    private class Itr implements Iterator<E> {
        int cursor = 0;   //将要访问的元素的索引
        int lastRet = -1;  //上一个访问元素的索引
        int expectedModCount = modCount;//expectedModCount为预期修改值,初始化等于modCount(AbstractList类中的一个成员变量)
    
        //判断是否还有下一个元素
        public boolean hasNext() {
                return cursor != size();
        }
        //取出下一个元素
        public E next() {
                checkForComodification();  //关键的一行代码,判断expectedModCount和modCount是否相等
            try {
            E next = get(cursor);
            lastRet = cursor++;
            return next;
            } catch (IndexOutOfBoundsException e) {
            checkForComodification();
            throw new NoSuchElementException();
            }
        }
    
        public void remove() {
            if (lastRet == -1)
            throw new IllegalStateException();
                checkForComodification();
    
            try {
            AbstractList.this.remove(lastRet);
            if (lastRet < cursor)
                cursor--;
            lastRet = -1;
            expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
            throw new ConcurrentModificationException();
            }
        }
    
        final void checkForComodification() {
            if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        }
        }
    

    根据代码可知,每次迭代list时,会初始化Itr的三个成员变量

    int cursor = 0;   //将要访问的元素的索引
    int lastRet = -1;  //上一个访问元素的索引
    int expectedModCount = modCount; //预期修改值,初始化等于modCount(AbstractList类中的一个成员变量)
    

    接着调用hasNext()循环判断访问元素的下标是否到达末尾。如果没有,调用next()方法,取出元素。
    而最上面测试代码出现异常的原因在于,next()方法调用checkForComodification()时,发现了modCount != expectedModCount

    接下来我们看下ArrayList的源码,了解下modCount 是如何与expectedModCount不相等的。

    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);  
    }  
      
    private void ensureExplicitCapacity(int paramInt) {  
        this.modCount += 1;    //修改modCount  
        /** 省略此处代码 */  
    }  
      
    public boolean remove(Object paramObject) {  
        int i;  
        if (paramObject == null)  
            for (i = 0; i < this.size; ++i) {  
                if (this.elementData[i] != null)  
                    continue;  
                fastRemove(i);  
                return true;  
            }  
        else  
            for (i = 0; i < this.size; ++i) {  
                if (!(paramObject.equals(this.elementData[i])))  
                    continue;  
                fastRemove(i);  
                return true;  
            }  
        return false;  
    }  
      
    private void fastRemove(int paramInt) {  
        this.modCount += 1;   //修改modCount  
        /** 省略此处代码 */  
    }  
      
    public void clear() {  
        this.modCount += 1;    //修改modCount  
        /** 省略此处代码 */  
    }  
    

    从上面的代码可以看出,ArrayList的add、remove、clear方法都会造成modCount的改变。迭代过程中如何调用这些方法就会造成modCount的增加,使迭代类中expectedModCount和modCount不相等。

    异常的解决

    1. 单线程环境

    好,现在我们已经基本了解了异常的发送原因了。接下来我们来解决它。
    我很任性,我就是想在迭代集合时删除集合的元素,怎么办?

    Iterator<String> iter = list.iterator();
    while(iter.hasNext()){
        String str = iter.next();
          if( str.equals("B") )
          {
            iter.remove();
          }
    }
    

    细心的朋友会发现Itr中的也有一个remove方法,实质也是调用了ArrayList中的remove,但增加了expectedModCount = modCount;保证了不会抛出java.util.ConcurrentModificationException异常。

    但是,这个办法的有两个弊端
    1.只能进行remove操作,add、clear等Itr中没有。
    2.而且只适用单线程环境。

    2. 多线程环境

    在多线程环境下,我们再次试验下上面的代码

    public class Test2 {
        static List<String> list = new ArrayList<String>();
    
        public static void main(String[] args) {
            list.add("a");
            list.add("b");
            list.add("c");
            list.add("d");
    
            new Thread() {
                public void run() {
                    Iterator<String> iterator = list.iterator();
    
                    while (iterator.hasNext()) {
                        System.out.println(Thread.currentThread().getName() + ":"
                                + iterator.next());
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                };
            }.start();
    
            new Thread() {
                public synchronized void run() {
                    Iterator<String> iterator = list.iterator();
    
                    while (iterator.hasNext()) {
                        String element = iterator.next();
                        System.out.println(Thread.currentThread().getName() + ":"
                                + element);
                        if (element.equals("c")) {
                            iterator.remove();
                        }
                    }
                };
            }.start();
    
        }
    }
    

    Output:


    异常的原因很简单,一个线程修改了list的modCount导致另外一个线程迭代时modCount与该迭代器的expectedModCount不相等。

    此时有两个办法:

    1. 迭代前加锁,解决了多线程问题,但还是不能进行迭代add、clear等操作
    public class Test2 {
        static List<String> list = new ArrayList<String>();
    
        public static void main(String[] args) {
            list.add("a");
            list.add("b");
            list.add("c");
            list.add("d");
    
            new Thread() {
                public void run() {
                    Iterator<String> iterator = list.iterator();
    
                    synchronized (list) {
                        while (iterator.hasNext()) {
                            System.out.println(Thread.currentThread().getName()
                                    + ":" + iterator.next());
                            try {
                                Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                            }
                        }
                    }
                };
            }.start();
    
            new Thread() {
                public synchronized void run() {
                    Iterator<String> iterator = list.iterator();
    
                    synchronized (list) {
                        while (iterator.hasNext()) {
                            String element = iterator.next();
                            System.out.println(Thread.currentThread().getName()
                                    + ":" + element);
                            if (element.equals("c")) {
                                iterator.remove();
                            }
                        }
                    }
                };
            }.start();
    
        }
    }
    
    1. 采用CopyOnWriteArrayList,解决了多线程问题,同时可以add、clear等操作
    public class Test2 {
        static List<String> list = new CopyOnWriteArrayList<String>();
    
        public static void main(String[] args) {
            list.add("a");
            list.add("b");
            list.add("c");
            list.add("d");
    
            new Thread() {
                public void run() {
                    Iterator<String> iterator = list.iterator();
    
                        while (iterator.hasNext()) {
                            System.out.println(Thread.currentThread().getName()
                                    + ":" + iterator.next());
                            try {
                                Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                            }
                        }
                };
            }.start();
    
            new Thread() {
                public synchronized void run() {
                    Iterator<String> iterator = list.iterator();
    
                        while (iterator.hasNext()) {
                            String element = iterator.next();
                            System.out.println(Thread.currentThread().getName()
                                    + ":" + element);
                            if (element.equals("c")) {
                                list.remove(element);
                            }
                        }
                };
            }.start();
    
        }
    }
    

    CopyOnWriteArrayList也是一个线程安全的ArrayList,其实现原理在于,每次add,remove等所有的操作都是重新创建一个新的数组,再把引用指向新的数组。
    由于我用CopyOnWriteArrayList少,这里就不多讨论了,想了解可以看:Java并发编程:并发容器之CopyOnWriteArrayList

    深入理解异常—fail-fast机制

    到这里,我们似乎已经理解完这个异常的产生缘由了。
    但是,仔细思考,还是会有几点疑惑:

    1. 既然modCount与expectedModCount不同会产生异常,那为什么还设置这个变量
    2. ConcurrentModificationException可以翻译成“并发修改异常”,那这个异常是否与多线程有关呢?

    我们来看看源码中modCount的注解

    /**
         * The number of times this list has been <i>structurally modified</i>.
         * Structural modifications are those that change the size of the
         * list, or otherwise perturb it in such a fashion that iterations in
         * progress may yield incorrect results.
         *
         * <p>This field is used by the iterator and list iterator implementation
         * returned by the {@code iterator} and {@code listIterator} methods.
         * If the value of this field changes unexpectedly, the iterator (or list
         * iterator) will throw a {@code ConcurrentModificationException} in
         * response to the {@code next}, {@code remove}, {@code previous},
         * {@code set} or {@code add} operations.  This provides
         * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
         * the face of concurrent modification during iteration.
         *
         * <p><b>Use of this field by subclasses is optional.</b> If a subclass
         * wishes to provide fail-fast iterators (and list iterators), then it
         * merely has to increment this field in its {@code add(int, E)} and
         * {@code remove(int)} methods (and any other methods that it overrides
         * that result in structural modifications to the list).  A single call to
         * {@code add(int, E)} or {@code remove(int)} must add no more than
         * one to this field, or the iterators (and list iterators) will throw
         * bogus {@code ConcurrentModificationExceptions}.  If an implementation
         * does not wish to provide fail-fast iterators, this field may be
         * ignored.
         */
        protected transient int modCount = 0;
    

    我们注意到,注解中频繁的出现了fail-fast
    那么fail-fast(快速失败)机制是什么呢?

    “快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

    看到这里,我们明白了,fail-fast机制就是为了防止多线程修改集合造成并发问题的机制嘛。
    之所以有modCount这个成员变量,就是为了辨别多线程修改集合时出现的错误。而java.util.ConcurrentModificationException就是并发异常。
    但是单线程使用不单时也可能抛出这个异常。

    参考:Java提高篇(三四)-----fail-fast机制

    相关文章

      网友评论

      • ktine:对用for()方法迭代产生的ConcurrentModificationException的原因有了一定的理解,但是在单线程的工作模式中,使用迭代模式去remove依然存在着这种异常问题。我把代码放在下面,你看下到底出现了什么原因
        ktine:private Node addNodes (Set < Node > nodes,int i, int j)
        {
        Node nodeA = null;
        Node nodeB = null;
        for (Node node : nodes)
        {
        if (node.getValue() == i)
        {
        nodeA = node;
        }
        else if (node.getValue() == j)
        {
        node.addPathIn();
        nodeB = node;
        }
        }
        if (nodeA == null)
        {
        nodeA = new Node(i);
        nodes.add(nodeA);
        }

        if (nodeB == null)
        {
        nodeB = new Node(j);
        nodeB.addPathIn();
        nodes.add(nodeB);
        }
        return nodeB;
        }
        ktine:public String order(int n, int[][] tasks)
        {
        if (tasks == null || tasks.length == 0)
        {
        return null;
        }
        StringBuffer sb = new StringBuffer();
        //记录每个节点的度
        Set<Node> nodes = new HashSet<>();
        //统计每个顶点的下一层节点
        Map<Integer, ArrayList<Node>> topology = new HashMap<>();
        //将入读为0的顶点的入列
        for (int i = 0; i < tasks.length; i++)
        {
        Node nodeB = addNodes(nodes, tasks[i][0], tasks[i][1]);
        if (topology.get(tasks[i][0]) == null)
        {
        ArrayList<Node> list = new ArrayList<>();
        list.add(nodeB);
        topology.put(tasks[i][0], list);
        }
        else
        {
        ArrayList<Node> list = topology.get(tasks[i][0]);
        list.add(nodeB);
        topology.put(tasks[i][0], list);
        }
        }
        Iterator it = nodes.iterator();
        while(it.hasNext())
        {
        Node node = it.next();

        if (node.getPathIn() == 0)
        {
        sb.append(node.getValue());
        nodes.remove(node);
        ArrayList<Node> subNodes = topology.get(node.getValue());
        if (subNodes != null)
        {
        for (int m = 0; m < subNodes.size(); m++)
        {
        subNodes.get(m).reducePathIn();
        }
        }
        }
        }
        if(nodes.size() > 0)
        {
        return null;
        }
        else
        {
        return sb.toString();
        }
        }
      • d2da27ad2d97:”迭代前加锁,解决了多线程问题,但还是不能进行迭代add、clear等操作“
        这个应该是有问题的下面的删除线程先获得锁执行,上面的遍历输出线程后执行还是会出现并发修改异常的。
        MrDTree:下面的删除线程执行的:`iterator.remove();`,这个时候`expectedModCount = modCount;`。上面的迭代线程迭代时应该是不会报错的
      • 杨进京:你怎么解释删除A却不会报错?
        MetaData:```
        public class AddRemoveListElement {
        public static void main(String args[]) {
        List<String> list = new ArrayList<String>();
        list.add("A");
        list.add("B");
        list.add("C");
        //
        // for (String s : list) {
        // if (s.equals("A")) {
        // list.remove(s);
        // }
        // }

        //foreach循环等效于迭代器
        Iterator<String> iterator=list.iterator();
        while(iterator.hasNext()){
        String s=iterator.next();
        if (s.equals("B")) {
        list.remove(s);
        }
        }
        }
        }
        ```
        看到这,估计也能猜个差不多。没产生异常的原因在于,iterator.hasNext 是false (cursor != size)
        没有执行到next() 方法

      本文标题:java.util.ConcurrentModification

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