浅析ConcurrentModificationException异常的原因


在分析前我们先做几个测试

public class TestList {

    public static void main(String[]args){
      List<String> list = new ArrayList<String>();
      list.add("1");list.add("2");
      list.add("3");list.add("4");  

      TestList.testFor(list);
      TestList.testForeach(list);
      TestList.testIteratorForListRemove(list);
      TestList.testIteratorForIteratorRemove(list);

   }
   //测试For循环
   public static void testFor(List<String> list){
      try{
        for(int i=0;i<list.size();i++){
            System.out.println("长度:"+list.size());
            if(list.contains("1")){
                list.remove(i);
            }
            System.out.println("长度:"+list.size());
            System.out.println("-----------------------------");
        }
      }catch(ConcurrentModificationException e){
        System.out.println("出现ConcurrentModificationException异常");
      }
 }
 //测试Foreach循环
 public static void testForeach(List<String> list){
    try{
        for(String str:list){
            System.out.println("长度:"+list.size());
            if(list.contains("1")){
                list.remove(str);
            }
            System.out.println("长度:"+list.size());
            System.out.println("-----------------------------");
        }
    }catch(ConcurrentModificationException e){
        System.out.println("出现ConcurrentModificationException异常");
    }
 }
 //测试迭代器使用List的remove方法
 public static void testIteratorForListRemove(List<String> list){
    try{
        Iterator<String>  it = list.iterator();
        while(it.hasNext()){
            System.out.println("长度:"+list.size());
            String str = it.next();
            if(list.contains("1")){
                list.remove(str);
            }
            System.out.println("长度:"+list.size());
            System.out.println("-----------------------------");
        }
    }catch(ConcurrentModificationException e){
        System.out.println("出现ConcurrentModificationException异常");
    }
 }
 //测试迭代器使用迭代器的remove方法
 public static void testIteratorForIteratorRemove(List<String> list){
    try{
        Iterator<String>  it = list.iterator();
        while(it.hasNext()){
            System.out.println("长度:"+list.size());
            String str = it.next();
            if(list.contains("1")){
                it.remove();
            }
            System.out.println("长度:"+list.size());

            System.out.println("-----------------------------");
        }
    }catch(ConcurrentModificationException e){
        System.out.println("出现ConcurrentModificationException异常");
    }
 }
}

结果展示

//For循环
长度:4
长度:3
-----------------------------
长度:3
长度:3
-----------------------------
长度:3
长度:3
-----------------------------
//Foreach循环(捕获异常)
长度:4
长度:3
-----------------------------
出现ConcurrentModificationException异常

//迭代器List的remove(捕获异常)
长度:4
长度:3
-----------------------------
长度:3
出现ConcurrentModificationException异常

//迭代器的remove
长度:4
长度:3
-----------------------------
长度:3
长度:3
-----------------------------
长度:3
长度:3
-----------------------------
长度:3
长度:3
-----------------------------

通过结果显示我们能很清楚的知道使用Foreach使用迭代器且使用List自身的remove方法会抛出ConcurrentModificationException异常。
      那么为什么会是这样的结果呢?首先先分析这四个遍历的情况,第一个For循环是最基础的循环,其他三个都是使用了迭代器进行循环,而抛出的异常的都是使用了迭代器的循环,由此我们可以推断产生异常的原因和迭代器内部机制有很大的关系,这就好办了,现在我们来看看Iterator迭代器中的源码情况,在这里注意一下expectedModCount、modCount这两个变量,抛出异常就是它们两个不相等,下面详细讲解为什么会不相等。

//源码
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;

    Itr() {}

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

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

    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> consumer) {
        Objects.requireNonNull(consumer);
        final int size = ArrayList.this.size;
        int i = cursor;
        if (i >= size) {
            return;
        }
        final Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        }
        while (i != size && modCount == expectedModCount) {
            consumer.accept((E) elementData[i++]);
        }
        // update once at end of iteration to reduce heap write traffic
        cursor = i;
        lastRet = i - 1;
        checkForComodification();
    }

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

通过上面的测试示例我们可以很清楚的知道会用到源码中的hashNext、next、remove等方法,先分析Foreach循环,Foreach循环的原理是默认会调用迭代器中的hashNext和next方法,首先先用hashNext方法判断是否还有下一个元素,如果有,就调用next方法,获取这个元素并赋值给冒号前面的变量,如此重复直至hashNext为fasle的时候就会结束循环。因此Foreach循环会调用迭代器中的两个方法hashNext和next,hashNext方法我们看源码没有什么毛病,重点是在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];
}
final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
}

首先看第一行它会调用checkForComodification方法进行检查modCount和expectedModCount是否相等,不相等就会抛出异常。这时候你会有疑问了(在源码中它们是相等的啊)?

int expectedModCount = modCount;

没错在迭代器中它们是相等的,但是你调用了list.remove(index)方法,注意看下面代码remove方法的第二行,modCount++;自增了,当list调用了remove方法后Foreach循环会调用迭代器中的next方法,next方法会调用checkForComodification方法进行判断所以就会抛出ConcurrentModificationException异常。

 //ArrayList中的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;
}

那么为什么第四个方法也就是使用了迭代器本身的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();
        }
    }

迭代器中的已经对这两个变量做了处理让它们始终是相等的。

总结

  1、尽量不要在遍历的时候修改集合中的元素
  2、如果硬要修改就使用迭代器本身自带的修改元素的方法  
  3、哈哈,选择for循环是最稳的