问题来源

   今天我在写项目时,对集合中的元素做了以下操作
在这里插入图片描述

在这里插入图片描述
报错信息如图
在这里插入图片描述
问题来源:
在Java中对集合进行遍历时,一旦对集合进行增删操作,在遍历时会引起java.util.ConcurrentModificationException: null异常。

问题原因

首先我们需要了解什么是Java集合的快速失败机制 “fail-fast”和安全失败机制“failsafe”

Java集合的快速失败机制 “fail-fast”和安全失败机制“failsafe”是什么?

  • 快速失败
    Java的快速失败机制是Java集合框架中的一种错误检测机制,当多个线程同时对集合中的内容进行修改时可能就会抛出 ConcurrentModificationException 异常。其实不仅仅是在多线程状态下,在单线程中用增强 for循环中一边遍历集合一边修改集合的元素也会抛出ConcurrentModificationException异常。看下面代码:
public class Main{
    public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
        for(Integer i : list){
            list.remove(i);  //运行时抛出ConcurrentModificationException异常
       }
   }
}

正确的做法是用迭代器的 remove() 方法,便可正常运行。

public class Main{
    public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    Iterator<Integer> it = list.iterator();
        while(it.hasNext()){
            it.remove();
       }
   }
}

造成这种情况的原因是什么?细心的同学可能已经发现两次调用的 remove() 方法不同,一个带参数据,一个不带参数,这个后面再说,经过查看 ArrayList 源码,找到了抛出异常的代码

final void checkForComodification() {
      if (modCount != expectedModCount)
     	throw new ConcurrentModificationException();
}

从上面代码中可以看到如果 modCount 和 expectedModCount 这两个变量不相等就会抛出ConcurrentModificationException 异常。那这两个变量又是什么呢?继续看源码

protected transient int modCount = 0; //在AbstractList中定义的变量
int expectedModCount = modCount;//在ArrayList中的内部类Itr中定义的变量

从上面代码可以看到,modCount初始值为0,而expectedModCount 初始值等于 modCount 也就是说在遍历的时候直接调用集合的 remove() 方法会导致 modCount 不等于 expectedModCount进而抛出ConcurrentModificationException 异常,而使用迭代器的 remove() 方法则不会出现这种问题。那么只能在看看 remove() 方法的源码找找原因了

 public E remove(int index) {
        rangeCheck(index);
        modCount++;
        E oldValue = elementData(index);
        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
        return oldValue;
   }

从上面代码中可以看到只有 modCount++ 了,而 expectedModCount 没有操作,当每一次迭代时,迭代器会比较 expectedModCount 和 modCount 的值是否相等,所以在调用 remove() 方法后,modCount 不等于 expectedModCount 了,这时就了报 ConcurrentModificationException 异常。但用迭代器中 remove() 的方法为什么不抛异常呢?原来迭代器调用的 remove() 方法和上面的 remove() 方法不是同一个!迭代器调用的 remove() 方法长这样:

public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;    //这行代码保证了expectedModCount和modCount是相等的
           } catch (IndexOutOfBoundsException ex) {
           		throw new ConcurrentModificationException();
           }
       }

从上面代码可以看到 expectedModCount = modCount ,所以迭代器的 remove() 方法保证了expectedModCount 和 modCount 是相等的,进而保证了在增强 for 循环中修改集合内容不会报ConcurrentModificationException 异常。

上面介绍的只是单线程的情况,用迭代器调用 remove() 方法即可正常运行,但如果是多线程会怎么样呢?

答案是在多线程的情况下即使用了迭代器调用 remove() 方法,还是会报ConcurrentModificationException 异常。这又是为什么呢?还是要从 expectedModCount 和modCount 这两个变量入手分析,刚刚说了 modCount 在 AbstractList 类中定义,而expectedModCount 在 ArrayList 内部类中定义,所以 modCount 是个共享变量而
expectedModCount 是属于线程各自的。简单说,线程1更新了 modCount 和属于自己的expectedModCount ,而在线程2看来只有 modCount 更新了, expectedModCount 并未更新,所以会抛出 ConcurrentModificationException 异常。

  • 安全失败
    采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会抛出ConcurrentModificationException异常。缺点是迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生了修改,迭代器是无法访问到修改后的内容。java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用。

问题解决方法

如何边遍历边移除 Collection 中的元素?

从上文“快速失败机制”可知在遍历集合时如果直接调用 remove() 方法会抛出ConcurrentModificationException 异常,所以使用迭代器中调用 remove() 方法。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐