ConcurrentModificationException异常解析和解决方法

首先,我也是在网上搜到的一篇博客了解到的ConcurrentModificationException的出现原因和解决办法,源地址连接https://www.cnblogs.com/dolphin0520/p/3933551.html,且文中的不少代码也是复制他的,但是一些总结都是自己的

ConcurrentModificationException

一、介绍

​ 在前面一篇文章中提到,对Vector、ArrayList在迭代的时候如果同时对其进行修改就会抛出java.util.ConcurrentModificationException异常。下面我们就来讨论以下这个异常出现的原因以及解决办法。

二、目录

  1. ConcurrentModificationException异常出现的原因
  2. 在单线程环境下的解决办法
  3. 在多线程环境下的解决方法

三、ConcurrentModificationException异常出现的原因

public class Test {
    public static void main(String[] args)  {
        ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(2);
        Iterator<Integer> iterator = list.iterator();
        while(iterator.hasNext()){
            Integer integer = iterator.next();
            if(integer==2)
                list.remove(integer);
        }
    }
}

运行后报错。

四、通过阅读源码查询原因

首先我们需要明确一点:不管是增强for还是什么,最终循环集合,都是使用的Iterator框架。

  1. 我们看看ArrayList.iterator()
public Iterator<E> iterator() {
  return new Itr();
}

返回的是Itr对象,然后我们发现Itr是ArrayList的一个内部类,下面我只展示部分对我们有用的方法

private class Itr implements Iterator<E> {
  int cursor;       // index of next element to return
  int lastRet = -1; // index of last element returned; -1 if no such
  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];
  }
  
  final void checkForComodification() {
    if (modCount != expectedModCount)
      throw new ConcurrentModificationException();
  }
}

我们再来确认下一些变量的含义:

cursor:表示下一个要访问的元素的索引,从next()方法的具体实现就可看出

lastRet:表示上一个访问的元素的索引

expectedModCount:表示对ArrayList修改次数的期望值,它的初始值为modCount。

modCount:是AbstractList类中的一个成员变量,通过ArrayList的add、remove会改变他的值

第一步:hasNext()

public boolean hasNext() {
    return cursor != size();
}

判断当前的索引是否是集合的最大size,如果不是则开始循环代码。如果下一个访问元素的下标等于ArrayList的大小,则肯定到达末尾了。

第二步:next

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];
  }

因为hasNext通过后,运行next方法,首先运行checkForComodification()方法,暂时我们先不看。

  1. 把cursor赋值给int i ,判断 i 是否超过会等于集合的最大数量,不过是则报错。
  2. 把ArrayList内部持有的数组赋值给elementData,判断 i 是否大于等于数据的最大数量,如果是,则报错。
  3. 最后把 cursor = i +1 基本可以看成 cursor++。然后返回数组中下标为 i 的数据。并且用lastRet去记录i,也就是将i的值看成上一次的操作的下标。

第三步:回顾checkForComodification

我们回顾next()方法中的第一步checkForComodification

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

判断modCount 和 expectedModCount是否一致。

我们还必须再看Itr对象的expectedModCount变量是如何赋值的

int expectedModCount = modCount;

原来,在Itr对象一实例化的时候,就把ArrayList的modCount给了expectedModCount。意思是只要modCount一发生改变,则就会出现ConcurrentModificationException的错误。那么modCount何时发生改变,就是我们报错的至关重要的原因了。其实上文也提到了ArrayList在add、remove的时候会去改变modCount的值,也就是说只要在调用ArrayList的add、remove方法后,再去调用Itr对象的next方法,都会报错。哈哈,和海子大神总结的一样哎。

顺着add方法找下去会找到modCount++;

public boolean add(E e) {
  ensureCapacityInternal(size + 1);  // Increments modCount!!
  elementData[size++] = e;
  return true;
}
private void ensureCapacityInternal(int minCapacity) {
  if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
  }
  ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
  modCount++;
  // overflow-conscious code
  if (minCapacity - elementData.length > 0)
    grow(minCapacity);
}

顺着remove方法找下去会找到modCount++;

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;
}
private void fastRemove(int index) {
  modCount++;
  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
}

五、在单线程环境下的解决办法

其实很简单,细心的朋友可能发现在Itr类中也给出了一个remove()方法:

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();
  }
}

根据ArrayList.this.remove(lastRet);其实调用的还是ArrayList的remove的方法,只是多了一步:expectedModCount = modCount;也就是每次remove的方法之前都会重新将两个用于对比的数据进行统一。故尝试,看看效果。

public class Test {
    public static void main(String[] args)  {
        ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(2);
        Iterator<Integer> iterator = list.iterator();
        while(iterator.hasNext()){
            Integer integer = iterator.next();
            if(integer==2)
                iterator.remove();   //注意这个地方
        }
    }
}
//成功

六、在多线程环境下的解决方法

上面的解决办法在单线程环境下适用,但是在多线程下适用吗?看下面一个例子:

public class Test {
    static ArrayList<Integer> list = new ArrayList<Integer>();
    public static void main(String[] args)  {
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        Thread thread1 = new Thread(){
            public void run() {
                Iterator<Integer> iterator = list.iterator();
                while(iterator.hasNext()){
                    Integer integer = iterator.next();
                    System.out.println(integer);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
        };
        Thread thread2 = new Thread(){
            public void run() {
                Iterator<Integer> iterator = list.iterator();
                while(iterator.hasNext()){
                    Integer integer = iterator.next();
                    if(integer==2)
                        iterator.remove(); 
                }
            };
        };
        thread1.start();
        thread2.start();
    }
}

报错原因:

有可能有朋友说ArrayList是非线程安全的容器,换成Vector就没问题了,实际上换成Vector还是会出现这种错误。

原因在于:你在2个线程中都有这么一个代码Iterator<Integer> iterator = list.iterator();从JVM的眼光看,2个线程,2个局部变量,则两个线程中的iterator是2个对象,那么iterator中的expectedModCount也是2个不同的,但是这2个expectedModCount都需要和同一个ArrayList的modCount进行对比,那么线程1在循环遍历,线程2在iterator.remove,则线程2的expectedModCount=1能和list的modCount=1保持一致,但是线程1的expectedModCount=0却不能和list的modCount=1保持一致,则报错

解决办法

  1. 使用synchronized保证有序性,将线程串行的执行,则将多线程强转为单线程,在使用iterator的remove方法则可以解决。
  2. 使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。

猜你喜欢

转载自blog.csdn.net/qq_38643434/article/details/82827526