ConcurrentModificationException异常原因和解决方法
首先看一下一个例子举出 ConcurrentModificationException 的出现场景:
public static void main(String[] args) { List<Integer> list = new ArrayList<Integer>(){ { add(1); add(2); add(3); } }; Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()) { iterator.next(); list.add(4); } } 1234567891011121314
ConcurrentModificationException 中文意思就是并发修改异常,存在于并发使用 Iterator 时出现的时候,那这个异常是为什么会出现的呢?这个涉及到 fast-fail 机制(快速失败),可以提前预料遍历失败情况,防止数组越界异常,我们看一下源代码:
我们看一下 remove 和 add 方法:
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 值,变量类似版本 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 } public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! //拷贝后面的值 System.arraycopy(elementData, index, elementData, index + 1, size - index); //添加元素 elementData[index] = element; size++; } private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } private void ensureExplicitCapacity(int minCapacity) { //修改 modCount 值,变量类似版本 modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
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]; } 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(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } } 123456789101112131415161718192021222324252627282930313233343536373839404142
modCount 是在 ArrayList 中赋值的,并且初始值为 0,在 add 和 remove 的时候(修改元素的时候)会增加 1,
我们看一下 hasNext 方法,在方法中的 size 是 ArrayList 中的变量,这个 ConcurrentModificationException 异常存在的原因之一就在这个方法体现出来了,在多线程的情况下,如果使用迭代器遍历时,ArrayList数组元素变少导致 cursor > size,然后数组越界。
public boolean hasNext() { return cursor != size; } 123
在正常情况下当 cursor == size代表已经到数组尽头了。我们再看一下 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]; } 1234567891011
next 方法中先是判断了 modCount 是否等于 expectedModCount,不相等则抛出 并发修改异常,否则取到游标,然后判断游标是否大于数组长度或者元素个数。最后游标加一,然后返回数的同时使 lastRet 等于 i。我们再看一下 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(); } } 1234567891011121314
其实就是在 remove 掉元素的时候更新下自己的 expectedModCount。注意的是remove后 lastRet 会变成 -1,也就是不能连续 remove 两次,因为在 next 中检查了 lastRet 的值不能小于 0。
public static void main(String[] args) { List<Integer> list = new ArrayList<Integer>(){ { add(1); add(2); add(3); } }; Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()) { iterator.next(); iterator.remove(); } } 123456789101112131415
private static List<Integer> LIST = new ArrayList<Integer>(){ { add(1); add(2); add(3); } }; public static void main(String[] args) { new Thread(() -> { Iterator<Integer> iterator = LIST.iterator(); while (iterator.hasNext()) { iterator.next(); iterator.remove(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(() -> { try { Thread.sleep(400); } catch (InterruptedException e) { e.printStackTrace(); } LIST.add(4); }).start(); } 1234567891011121314151617181920212223242526272829303132
有可能有朋友说ArrayList是非线程安全的容器,换成Vector就没问题了,实际上换成Vector还是会出现这种错误。
原因在于,虽然Vector的方法采用了synchronized进行了同步,但是实际上通过Iterator访问的情况下,每个线程里面返回的是不同的iterator,也即是说expectedModCount是每个线程私有。假若此时有2个线程,线程1在进行遍历,线程2在进行修改,那么很有可能导致线程2修改后导致Vector中的modCount自增了,线程2的expectedModCount也自增了,但是线程1的expectedModCount没有自增,此时线程1遍历时就会出现expectedModCount不等于modCount的情况了。
因此一般有2种解决办法:
在使用iterator迭代的时候使用synchronized或者Lock进行同步;
使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。