Java集合类的框架中,大多都可以用for 循环进行遍历元素,偶尔还在遍历中删除元素,稍有不慎,则会ConcurrentModificationException。这篇博文,聊聊小白对于ConcurrentModificationException异常和循环中删除集合元素的理解。
ConcurrentModificationException
当不允许这样的修改时,可以通过检测到对象的并发修改的方法来抛出此异常。
例如,一个线程通常不允许修改集合,而另一个线程正在遍历它。 一般来说,在这种情况下,迭代的结果是未定义的。 某些迭代器实现(包括由JRE提供的所有通用集合实现的实现)可能会选择在检测到此行为时抛出此异常。 这样做的迭代器被称为故障快速(fail-fast)迭代器(iterator),因为它们是快速而干净地失败的行为,而不是在未来不确定的时间冒着不确定风险的行为。
请注意,此异常并不总是表示对象已被不同的线程同时修改。 如果单个线程发出违反对象合同的方法调用序列,则该对象可能会抛出此异常。 例如,如果线程在使用故障快速迭代器迭代集合时直接修改集合,则迭代器将抛出此异常。
请注意,故障快速行为无法保证,因为一般来说,在不同步并发修改的情况下,无法做出任何硬性保证。 失败快速的操作ConcurrentModificationException
抛出ConcurrentModificationException
。 因此,编写依赖于此异常的程序的正确性将是错误的: ConcurrentModificationException
应仅用于检测错误。
以上内容,来自ConcurrentModificationException类的注释说明,要点总结如下:
1、一个线程遍历集合,一个线程修改的时候会快速失败
2、单个线程遍历和修改同时操作,会快速失败
3、ConcurrentModificationException仅用于错误检测,不应当做编码正确的依赖
异常原因
取一个ArrayList的例子,来分析抛ConcurrentModificationException的原因,如下代码会抛出ConcurrentModificationException异常
List<String> list = new ArrayList<>();
list.add("001");
list.add("002");
list.add("003");
for (String value : list) {
if ("003".equals(value)) {
list.remove(value);
}
}
运行,可以看到错误日志信息
[001, 002, 003]
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.it.IteratorMain.main(IteratorMain.java:16)
到ArrayList类第909行看看,此方法校验遍历过程中,集合是否被修改
modCount 为此列表在结构上被修改的次数。结构修改是指改变列表的大小,包括增加元素和删除元素,或者以某种方式干扰列表,使得在进行中的迭代可能产生不正确的结果。
此字段由迭代器(iterator)和列表迭代器(list iterator)返回的迭代器实现。如果如果此字段的值发生意外更改,迭代器(或listiterator)将抛出ConcurrentModificationException,以响应next、remove、previous、set或add操作。这提供了快速失败的行为,而不是在迭代过程中面对并发修改时的不确定性行为。
再看一个ArrayList的foreach方法
@Override
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
forEach方法中,final int expectedModCount = modCount; 在遍历之前,首先获取一个expectedModCount,作为期望的修改次数,这个变量的值即modCount,将当前集合修改的次数赋予expectedModCount。后续的操作中,一旦modCount发生变化,立即抛出ConcurrentModificationException异常。
遍历中删除元素
如果遍历集合过程中删除元素会有ConcurrentModificationException风险,那遍历的时候是否可以删除呢,答案是肯定的。这里说几种遍历中可以删除元素的方法
1、 iterator迭代器删除
2、 for( ; ; )循环删除
iterator迭代器删除
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("001");
list.add("002");
list.add("003");
System.out.println(list);
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String value = it.next();
if ("003".equals(value)) {
it.remove();
}
}
System.out.println(list);
}
ArrayList中迭代器Iterator的remove方法注释内容有:迭代器的remove方法从集合中移除此迭代器返回的最后一个元素(可选操作)。每次调用next只能调用一次此方法。如果在操作进行过程中以调用此remove方法以外的任何方式修改了基础集合,则迭代器的行为未指定。由此可见,迭代器的remove方法是集合里面遍历删除的名门正派,其它的方法在遍历中修改集合,都可能给迭代器的行为带来不确定性。
看一下ArrayList中关于Iterator迭代器中remove方法的实现
/**
* An optimized version of AbstractList.Itr
*/
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; // 构造迭代器的时候,将modCount的值赋给expectedModCount
Itr() {}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification(); // 删除之前先检测expectedModCount和modCount是否相等
try {
ArrayList.this.remove(lastRet); // 删除元素
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount; // 删除后,将modCount再赋值给expectedModCount
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
根据ArrayList中iterator的源码可以看到,iterator每次在删除元素后,都会将expectedModCount 和 modCount对齐,所以,在迭代器iterator中remove元素不会抛异常。
for( ; ; )循环删除
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("001");
list.add("002");
list.add("003");
System.out.println(list);
for (int i=0; i<list.size(); i++) {
String value = list.get(i);
if ("003".equals(value)) {
list.remove(value); // list.remove(i)同样可以
}
}
System.out.println(list);
}
通过以上代码,可以看到普通for循环删除是可以删除掉list中的元素,但是通过普通for循环删除会带有一些不确定性,因为删除后list的长度变了,导致后续的操作可能会有风险。
如下代码,看似在for循环中删除list中所有元素,但是执行结果与预想不一致
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("001");
list.add("002");
list.add("003");
System.out.println(list);
for (int i=0; i<list.size(); i++) {
list.remove(i);
}
System.out.println(list);
}
执行结果,可以看到,list中有002这个元素被遗漏了
[001, 002, 003]
[002]
问题出在哪里,debug看一下,在 i 等于0,list做了第一次删除元素操作后,此时 i 自增为2,而list由之前的[001, 002, 003]变为了[002, 003],这样就删除不到原list中标号为1的元素了。
这个可能就是遍历列表时候修改列表结构时产生的不可预料的行为吧。
经过上面的掰扯,可以得出,如果要遍历中删除元素,最好是使用集合官方提供的Iterator迭代器里面的remove方法,这个方法保险系数高很多,会避免一些不可预料的问题出现。