• 集合遍历remove时ConcurrentModificationException异常


    1.集合遍历时候,有时候需要remove或add操作,这时候遍历方式可能会影响程序运行

      例如:

     @Test
        public void test1() {
            List<Integer> intList = new ArrayList<>();
            for (int i = 0; i < 20; i++) {
                intList.add(Integer.valueOf(i));
            }
    
            // 迭代器遍历, 异常
            Iterator<Integer> iterator_int = intList.iterator();
            while (iterator_int.hasNext()) {
                Integer integer = iterator_int.next();  //ConcurrentModificationException
                if (integer.intValue() == 5) {   //这里选择集合靠中间的数据操作
                    //intList.remove(integer); //这里使用集合的remove方法
                    intList.add(55);
                }
            }
    
            // foreach遍历, 异常
            for (Integer value : intList) {  //ConcurrentModificationException
                if (value.intValue() == 5) {
                    intList.remove(value);  //这里使用集合的remove方法
                }
            }
    
            //普通for循环 , 正常
            for (int i = 0; i < intList.size(); i++) {
                if (intList.get(i) == 5) {
                    intList.remove(intList.get(i));
                }
            }
    
        }

    2.为什么上面的迭代器和foreach遍历会有异常?

     首先,看迭代器方式遍历,在 iterator_int.next()  方法出报异常.看一下源码:

          

     ① 在父类AbstractList中定义了一个int型的属性:modCount

      protected transient int modCount = 0;

    ② 在ArrayList的所有涉及结构变化的方法中都增加modCount的值,包括:add()、remove()、addAll()、removeRange()及clear()方法。这些方法每调用一次,modCount的值就加1。

    注:add()及addAll()方法的modCount的值是在其中调用的ensureCapacity()方法中增加的。 

    ③AbstractList中的iterator()方法(ArrayList直接继承了这个方法)使用了一个私有内部成员类Itr,生成一个Itr对象(Iterator接口)返回:

    public Iterator iterator() { return new Itr(); }

    ④ Itr实现了Iterator()接口,其中也定义了一个int型的属性:expectedModCount,这个属性在Itr类初始化时被赋予ArrayList对象的modCount属性的值。

    int expectedModCount = modCount;

     注:内部成员类Itr也是ArrayList类的一个成员,它可以访问所有的AbstractList的属性和方法。理解了这一点,Itr类的实现就容易理解了。

    ⑤ checkForComodification (检查是否并发修改)

      /**
      * 在对一个集合对象进行跌代操作的同时,并不限制对集合对象的元素进行操作
      * 这些操作包括一些可能引起跌代错误的add()或remove()等危险操作。
      * 在AbstractList中,使用了一个简单的机制来规避这些风险。 
      * 这就是modCount和expectedModCount的作用所在
      */

    断点可以看到,remove操作后,modeCount=21, 而expectedModCount=20 不相等,抛出异常.

    下面是集合类的remove方法:

    public boolean remove(Object o) {
            if (o == null) {
                for (int index = 0; index < size; index++)
                    if (elementData[index] == null) {
                        fastRemove(index);
                        return true;
                    }
            } else {
                for (int index = 0; index < size; index++)
                    if (o.equals(elementData[index])) {
                        fastRemove(index);
                        return true;
                    }
            }
            return false;
        }

    fastRemove方法:
    /*
    * Private remove method that skips bounds checking and does not
    * return the value removed.
    */
    private void fastRemove(int index) {
    modCount++; //remove会加1,所以checkForComodification校验时候会异常
    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
    }

    3.foreach遍历是对迭代器封装了一下,所以也会报异常. 

    4. 普通for循环不会异常,因为使用的checkForComodification是另一个内部类的方法

    SubList :

     

     checkForComodification方法:

     

     5.fail-fast机制

    有两个线程(线程A,线程B),其中线程A负责遍历list、线程B修改list。 
    线程A在遍历list过程的某个时候(此时expectedModCount = modCount=N),线程启动, 
    同时线程B增加一个元素,这是modCount的值发生改变(modCount + 1 = N + 1)。 线程A继续遍历执行next方法时, 
    通告checkForComodification方法发现expectedModCount  = N  , 而modCount = N + 1,两者不等, 
    这时就抛出ConcurrentModificationException 异常,从而产生fail-fast机制。

    6.避免fail-fast机制

     ① 方法1

    在单线程的遍历过程中,如果要进行remove操作,可以调用迭代器的remove方法而不是集合类的remove方法。看看ArrayList中迭代器的remove方法的源码:
     public void remove() {
                        if (lastRet < 0)
                            throw new IllegalStateException();
                        checkForComodification();
    
                        try {
                            SubList.this.remove(lastRet);
                            cursor = lastRet;
                            lastRet = -1;
                            expectedModCount = ArrayList.this.modCount;
                        } catch (IndexOutOfBoundsException ex) {
                            throw new ConcurrentModificationException();
                        }
                    }

    可以看到,该remove方法并不会修改modCount的值,并且不会对后面的遍历造成影响,

    因为该方法remove不能指定元素,只能remove当前遍历过的那个元素,所以调用该方法并不会发生fail-fast现象。该方法有局限性。

    例:

     // intList.remove(integer);  //集合类remove
       iterator_int.remove(); //迭代器的remove()方法  

    方法2

            使用java并发包(java.util.concurrent)中的类来代替ArrayList 和hashMap。

            比如使用 CopyOnWriterArrayList代替ArrayList,CopyOnWriterArrayList在是使用上跟ArrayList几乎一样,CopyOnWriter是写时复制的容器(COW),在读写时是线程安全的。

    该容器在对add和remove等操作时,并不是在原数组上进行修改,而是将原数组拷贝一份,在新数组上进行修改,待完成后,才将指向旧数组的引用指向新数组,

    所以对于CopyOnWriterArrayList在迭代过程并不会发生fail-fast现象。但 CopyOnWrite容器只能保证数据的最终一致性不能保证数据的实时一致性

           对于HashMap,可以使用ConcurrentHashMap,ConcurrentHashMap采用了锁机制,是线程安全的。

    在迭代方面,ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,

    取而代之的是在改变时new新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据 ,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。

    即迭代不会发生fail-fast,但不保证获取的是最新的数据。

     

      

    --------------------- 
    参考:https://blog.csdn.net/zymx14/article/details/78394464  

    https://blog.csdn.net/weixin_40254498/article/details/81386920

  • 相关阅读:
    1.1下载
    【翻译】培训提示:解决常见编码问题的简单技巧
    小猪的Git使用总结
    Singleton单例模式是最简单的设计模式,它的主要作用是保证在程序执行生命周期中,使用了单类模式的类仅仅能有一个实例对象存在。
    springweb flux 编程模型
    java aop做一个接口耗时的计算
    mysql保留两位小数
    在OneThink(ThinkPHP3.2.3)中整合阿里云OSS的PHP-SDK2.0.4,实现Web端直传,服务端签名直传并设置上传回调的实现流程
    Windows下编译使用Aliyun OSS PHP SDK
    windows下Gulp入门详细教程 &&gulp安装失败的原因(红色)
  • 原文地址:https://www.cnblogs.com/coloz/p/11050723.html
Copyright © 2020-2023  润新知