ConcurrentModificiationException出现的原因分析
1.ConcurrentModificationException的抛出
当我们使用迭代器进行集合的遍历的时候,当你不小心在遍历的时候又对集合进行了增删改的操作的时候,注:此时不是使用迭代器的方式对集合进行的操作,就会发生如下的异常:
Exception in thread "main" java.util.ConcurrentModificationException:
ConcurrentModificationException:并发修改异常.
2. 以ArrayList源码解析迭代器的内部组成
那么究竟是怎么回事呢,现在我们来一探究竟.
我们以ArrayList为列:
先看这段源码:
//集合中的迭代器(继承了Iterator接口)
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];
}
我们从上面的源码中可以看出,当我们写出这么一段代码的时候:
//创建一个集合
ArrayList<Integer> list = new ArrayList<Integer>();
//往集合中添加一些元素
Collections.addAll(list, 1, 2, 3, 4, 5);
//获取集合的迭代器
Iterator<Integer> itr = list.iterator();
//进行迭代
while (itr.hasNext()) {
//有下一个元素,获取下个元素
Integer next = itr.next();
System.out.println(next);
//使用集合的方式对集合进行删除操作
if (next.equals(list.get(list.size()-3))) {
list.remove(next);
}
}
这里我们来一一说明下迭代器中的那些参数
cursor
:游标,用来指向下一个元素
lastRet
:获取最后一个元素的返回值
expectedModCount
:预期的修改次数
modCount
:记录集合的修改次数
size
:集合的长度
然后我们来看下迭代器中的public E next()
中的checkForComodification();
方法:这里
final void checkForComodification() {
//这里可以看出,当预期的次数和集合的修改次数不一致时,抛出此ConcurrentModificationException
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
这里解释一下,当我们使用Iterator<Integer> itr = list.iterator();
的方式获取迭代器的时候我们将集合的修改次数赋值给了迭代器中的预期修改次数.
3.迭代器的执行流程和异常发生的原因
现在我们来一步步解读下整个迭代的流程,以及可能出现的问题
首先第一步:我们获取迭代器,此时我们将集合中的modCount赋值给了迭代器中的expectedModCount;此时二者是相等的;然后我们调用迭代器的itr.hasNext()方法这是判断return cursor != size;显然返回true,这是后进入循环体,接着我们调用迭代器的next();方法获取到了集合中的元素;在这个方法中我们调用了checkForComodification()
方法来判断预期修改次数和集合的修改次数时候相等,显然相等,然后cursor+1;返回元素;当我们遍历中没有对集合进行其他的操作的时候,当遍历到集合的长度的时候hasNext()方法返回false结束循环.
接下来我们看看当我们在遍历的时候进行集合的操作的时候,会发现这么几个问题;当我们在集合的倒数第二个元素之前进行操作的时候,我们来看看:(这里特意说明一下集合的倒数第个)
我们的itr.hasNext()返回的是true;这时候看下源码:
//集合的删除原属操作
public E remove(int index) {
rangeCheck(index);
//我们在这里可以看出,当我们对集合进行删除的时候modCount++进行删除操作
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;
}
然后回到public E next()
方法;在进入之前先进行了checkForComodification()判断,此时
modCount != expectedModCount成立,抛出throw new ConcurrentModificationException();
这就是为什么我们在进行操作的时候发生并发修改异常.
当我们删除最后一个元素的时候:我们可以明显的发现,在判断itr.hasNext()方法时返回的true,我们来分析一下这个方法中所作的事情:删除最后一个元素size-1,而游标cursor在原来size的基础之上+1,显然itr.hasNext()返回的也是true;这个时候我们继续循环,当我们取元素的时候,这个时候再次调用checkForComodification()方法,我们由上面的基础可以知道,此时的预期修改次数显然不等于集合的修改次数,抛出ConcurrentModificationException();
那么还有一种特殊情况就是,当我们删除的是集合的倒数第二个元素的时候我们会发现;
在我们删除元素后size-1 和 cursor+1刚好相等,也就是说,在我们继续进行itr.hasNext()时候,返回的是false;此时循环结束,当然也就不会去调用next()方法,也就不会抛出throw new ConcurrentModificationException();
4.使用迭代器自身的修改操作避免发生并发修改异常
首先,我们还是来看一段源代码:
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();
}
}
我们在源代码中结合前面的只是可以得知,当我们保持了expectedModCount = modCount;
时,在检查时,不会抛出并发修改异常.
@author:胡盛金