java.util.ConcurrentModificationException
在进行如下代码的过程中发现了这样的一个异常:ConcurrentModificationException,即并发修改异常。
LinkedList<Integer> list=new LinkedList();
list.add(1);
list.add(1);
ListIterator<Integer> it=list.listIterator();
while(it.hasNext()){
Integer i=it.next();
list.remove(i);
}
提示我在it.next()处抛出异常
以下来自jdk 9 javadoc
当不允许这样的修改时,可以通过检测到对象的并发修改的方法来抛出此异常。
例如,一个线程通常不允许修改集合,而另一个线程正在遍历它。 一般来说,在这种情况下,迭代的结果是未定义的。 某些迭代器实现(包括由JRE提供的所有通用集合实现的实现)可能会选择在检测到此行为时抛出此异常。 这样做的迭代器被称为故障快速迭代器,因为它们快速而干净地失败,而是在未来未确定的时间冒着任意的非确定性行为。
请注意,此异常并不总是表示对象已被不同的线程同时修改。 如果单个线程发出违反对象合同的方法调用序列,则该对象可能会抛出此异常。 例如,如果线程在使用故障快速迭代器迭代集合时直接修改集合,则迭代器将抛出此异常。
请注意,故障快速行为无法保证,因为一般来说,在不同步并发修改的情况下,无法做出任何硬性保证。 失败快速的操作ConcurrentModificationException抛出ConcurrentModificationException 。 因此,编写依赖于此异常的程序的正确性将是错误的: ConcurrentModificationException应仅用于检测错误。
问题是怎么发生的?
官方的文档当然不能解决我的问题。来看看问题到底是怎么发生的,
迭代器的源码:
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;
private Node<E> next;
private int nextIndex;
private int expectedModCount = modCount;
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}
public boolean hasNext() {
return nextIndex < size;
}
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
public boolean hasPrevious() {
return nextIndex > 0;
}
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
lastReturned = next = (next == null) ? last : next.prev;
nextIndex--;
return lastReturned.item;
}
public int nextIndex() {
return nextIndex;
}
public int previousIndex() {
return nextIndex - 1;
}
public void remove() {
checkForComodification();
if (lastReturned == null)
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;
unlink(lastReturned);
if (next == lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}
public void set(E e) {
if (lastReturned == null)
throw new IllegalStateException();
checkForComodification();
lastReturned.item = e;
}
public void add(E e) {
checkForComodification();
lastReturned = null;
if (next == null)
linkLast(e);
else
linkBefore(e, next);
nextIndex++;
expectedModCount++;
}
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (modCount == expectedModCount && nextIndex < size) {
action.accept(next.item);
lastReturned = next;
next = next.next;
nextIndex++;
}
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
可以发现是由于名为expectedModCount 和 modCount的变量不一致造成的,即预期修改数和实际修改数,在迭代器的代码中,两者都是同步变化的,因此迭代器的使用不会造成并发修改异常。
再次检查我的代码发现我使用了LinkedList的remove方法,问题就出在这里:
modcount是属于LinkedList类的,而ListItr是一个LinkedList类的内部类,expectedModCount只属于listltr类,
linkedlist的所有会对存储内容改变的函数都会更新modcount,把modcount加1,但是不会修改专属于迭代器类的expectedmodcount,
同时迭代器的也有一套对应的修改存储内容的方法,比如remove、add,但是它们都同时修改expectedModCount 和 modCount,即维护了预期与实际修改数。
因为迭代器类和其所属容器类共享一个modCount,而容器并不对expectedmodcount负责,
这就表示在迭代器使用之中夹杂其所属容器的能修改存储内容的方法是被拒绝的。
解决方法也很简单,使用迭代器提供的对应方法就可以了。
为什么这么设计呢?
这种设计是因为此容器实现不同步。 如果多个线程同时访问链接列表,并且至少有一个线程在结构上修改列表,则必须在外部进行同步。而这明显增加了风险。
如果列表在迭代器创建后的任何时间进行结构修改,除了通过Iterator自己的remove或add方法外,迭代器将抛出一个ConcurrentModificationException 。
那么就能保证在迭代器使用期间,对存储内容的修改是单线程的 ,但是由于它只是简单的抛出异常,迭代器的这种行为应仅用于检测错误。
相关知识能在并发容器-迭代器找到
thinking in java 太老了,还是java核心技术吧。