集合的并发修改异常

Guo_hong / 2023-05-10 / 原文

情景一:

ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 10_000; i++) {
	arrayList.add(new Random().nextInt(100_000_000));
}

/**
 开启多个线程,每个线程都执行迭代器
 */
for (int i = 0; i < 20; i++) {
	new Thread(()->{
		Iterator<Integer> iterator = arrayList.iterator();
		while (iterator.hasNext()){
			Integer next = iterator.next();
			if(next > 1_000_000){
				iterator.remove();
			}
		}
	}).start();
}

结果会抛出一推异常,比如

Exception in thread "Thread-0" java.util.ConcurrentModificationException...

为什么在集合的迭代器中修改元素会抛出 ”并发修改异常”?

首先,了解一个 AbstractList 的成员变量:

protected transient int modCount = 0;

再看看 ArrayList 的迭代器中的一个成员变量:

private class Itr implements Iterator<E> {
    //...
    int expectedModCount = modCount;
    //..

我没每次调用迭代器 next() \ remove() ..等一些方法时,这些方法内部都会调用一个检查方法 checkForComodification():

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

从这里可以看出 expectedModCount 和 modCount 的关系,一个集合可以实例化出多个迭代器,但一个集合只能有一份 modCount 成员变量, 也就是说 多个迭代器共享一个 modCount。

modCoune 和 expectedModCount 要保持一致,否者就会抛出 并发修改异常。

那又是什么情况导致 modCoune 发生变化呢?

案例中,我们是多个线程同时迭代并删除符合条件的元素,那么具体看看 ArrayList 中迭代器的 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();
	}
}

发现迭代器的 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,每次调用 remove(),modCount 就会自增1。

从而得知,当有多个线程执行迭代器来删除元素时,就会导致 modCount 的混乱,从而发生并发修改异常。

当然,这时其中的一种情况,还有一种情况:

我们重点关注迭代器的 remove() 其中另外一种发生并发修改异常的情况:

public void remove() {
	// ...
	try {
		ArrayList.this.remove(lastRet);
		cursor = lastRet;
		lastRet = -1;
		expectedModCount = modCount;
        // !!!
	} catch (IndexOutOfBoundsException ex) {
		throw new ConcurrentModificationException();
	}
}

可以看出,try 代码快可能会发生 IndexOutOfBoundsException[1]

就是, 而导致的 并发修改异常, 那又是什么情况导致 IndexOutOfBoundsException 呢?

还是多线成引起的问题,这很好解释,假如一个集合 list 有5个元素,有两个线程同时遍历迭代器操作集合,A线程再遍历到第3个元素时,发现符合条件并删除这个元素,迭代器是根据 size[2]来遍历集合的,A线程改变了集合的 size,可是B线程却不知道 size 已经被改变,结果总会发生 索引越界异常

情景二:

另外,在增强for循环中修改集合元素也会抛出并发修改异常:

ArrayList<String> arrayList = new ArrayList<>();
Collections.addAll(arrayList, "tom", "kobe", "jordan", "tracy", "westbook");
for(String s : arrayList){
	if("jordan".equals(s)){
		// arrayList.remove(s);
		arrayList.add(s);
	}
}

Exception in thread "main" java.util.ConcurrentModificationException

可以看到,在单线程里,通过增强 for 循环来修改集合元素,还是会抛出并发修改异常,这是为啥?

首先,我们用集合本身的修改元素方法,就会导致 modCount 的增加,

我们有没有用迭代器的方法来修改集合元素,就会导致 expectedModCount 与 modCount 不能得到同步,

但是!增强 for 循环遍历集合,本质上就是用集合的迭代器来遍历集合,其中,一定用到了 hasNext() \ next(), 而它们中都调用了 checkForComodification() 来判断 expectedModCount 与 modCount,从而导致 ConcurrentModificationException[3],

同时也再次强调:增强 for 循环遍历集合,最多读取集合的元素,不要试图去修改集合的元素!

@脚注


  1. 索引越界异常 ↩︎

  2. 集合的长度 ↩︎

  3. 并发修改异常 ↩︎