集合遍历remove时ConcurrentModificationException异常

大憨熊 提交于 2020-12-25 02:42:34

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

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!