关于ArrayList的ConcurrentModificationException的一些思考

关于ArrayList的ConcurrentModificationException的一些思考

先来看一个来自于阿里java规范文档的例子:

        List a = new ArrayList();
        a.add("1");
        a.add("2");
        for (String temp : a) {
            if("1".equals(temp)){
                a.remove(temp);
            }
        }

这里的执行结果比较奇怪。(待会再分析)


分析ArrayList的源码,可以看到其(或者父类AbstractList)维护一个modCount的变量:

protected transient int modCount = 0;

根据文档,这个变量的作用是

The number of times this list has been structurally modified.
Structural modifications are those that change the size of the list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.
这个数字的描述的是list的结构性修改次数,结构性修改指的是,那些会改变list的长度或者破坏list的方式,使得正在进行的迭代产生错误结果。

我们来看看ArrayList中为什么会抛出ConcurrentModificationException

  • I remove()方法(改变modCount值)
    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;
    }

以及add()方法

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;//look这里

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

可以看出,remove()的时候是对modCount进行过修改,同样的在add()方法中也是,即添加删除多少次modCount就是多少。我们并没有看到ConcurrentModificationException的抛出,说明,无论我们怎么对ArrayList进行修改,只会改变modCount值而不会抛出异常。
那么是哪里抛出的呢,看下面:

  • II 迭代器Itr(抛出ConcurrentModificationException异常)
 private class Itr implements Iterator {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;//获取迭代器的时候会初始化expectedModCount

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

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();//检查list是否被操作过,若没有继续
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                //数据没有被操作而合法的游标cursor又大于数组的长度,日了狗了,hasNext()为什么不用return cursor < size
                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 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();
        }
    }

从上面的代码可以看出在迭代器Itr的next(), remove()这两个方法在进行操作前都会去使用checkForComodification校验modCount是否一致,即保证当前操作前的list没有被改变,就像每次出门进门都会检查锁坏没有一样,若没有坏(modCount == expectedModCount)就继续开门锁门,若坏了(modCount != expectedModCount),就报警或者找修锁的(抛出ConcurrentModificationException)。

所以在多线程环境中,一个线程要添加删除,另一个线程迭代,很容易中招。


回到最开始的例子,这个例子可以正常的执行下去,最后a里只剩一个元素’2’。但是啊但是,若改成

...
if(“2”.equals(temp)) {
   //......
}
...

此时运行会抛出ConcurrentModificationException,这跟我们刚刚分析的不一样啊,明明没有用迭代器,怎么会抛出ConcurrentModificationException。
没办法,看看异常信息吧,如下:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at cc.rinoux.instantTest.main(instantTest.java:16)
......

果然还是迭代器产生的异常。什么时候调的迭代器?当然是foreach语句,foreach语句实际上是对iterator.hasNext()和next()的调用,先判断hasNext(),为true再next()获得元素(并非语法糖,涉及到Collection和Iterable,此处不赘述)。

但是为什么if(“1”.equals(temp))没有抛异常,而if(“2”.equals(temp))抛异常,这就涉及到上面注释里写的hasNext()为什么要这样写。

前者在完成第一次next操作后,cursor变为1,在remove,size变为1;继续遍历,先做hasNext判断,此时size为1 == cursor,直接返回false跳出循环遍历中止,因此没机会去抛出 ConcurrentModificationException。

后者则是,第一个遍历结果,不满足remove条件,因此modCount依然为0,hasNext()依然为true;第二个遍历结果满足,remove,modCount为1, size为1, cursor为2;在下一次foreach中,hasNext()通过,但是next()就不行了,因为modCount != expectedModCount,所以操作前检查抛出ConcurrentModificationException。
注意这里,cursor是大于size的,游标大于list长度的情况就发生了,所以hasNext()用的return cursor != size 而非 return cursor < size。

你可能感兴趣的:(关于ArrayList的ConcurrentModificationException的一些思考)