情景一:
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 循环遍历集合,最多读取集合的元素,不要试图去修改集合的元素!
@脚注