一、快速失败(fail-fast)
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出ConcurrentModificationException。
1️⃣原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。
2️⃣注意:这里异常的抛出条件是检测到modCount!=expectedmodCount
这个条件。如果集合发生变化时,修改 modCount 值刚好又设置为 expectedmodCount 值,则不会抛出异常。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的 bug。
3️⃣场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。
二、安全失败(fail-safe)
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
1️⃣原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException。
2️⃣缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
3️⃣场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
三、引出问题
请看下面两段代码,哪段代码会报错?
List<String> listA = new ArrayList<String>();
listA.add("1");
listA.add("2");
for (String s : listA) {
if("1".equals(s)){
listA.remove(s);
}
}
1️⃣listA的remove方法成功执行。
List<String> listB = new ArrayList<String>();
listB.add("2");
listB.add("1");
for (String s : listB) {
if("1".equals(s)){
listB.remove(s);
}
}
2️⃣listB的remove方法运行抛出ConcurrentModificationException异常。
四、为了寻找原因,查看源码
增强 for 循环,其实是依赖了 while 循环和 Iterator 实现的。所有的 Collection 集合类都会实现 Iterable 接口。找到 ArrayList 类的 Iterator():使用自己的 Itr 内部类,并且实现了 Iterator 接口。迭代器的本质是先调用 hasNext() 判断是否存在下一个元素,然后再使用 next() 取下一个元素:Itr 内部类实现
/**
* 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;
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];
}
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();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
1️⃣listA 能 remove 成功,是因为它只循环了一次。因为它在 remove 元素 1 之后,它的 size - 1 变成 1,然后 Itr 内部的 cursor 变量由 0 变成 1。此时 1 = 1,循环结束,所以成功了。
2️⃣listB 却 remove 失败,其实它在循环第二次的时候,也 remove 成功了,但是第三次判断 next 的时候 cursor 的值为 2 导致不等于现在的 size 1,所以执行了 next 方法。最重要的来了,之前 remove 的操作导致 ArrayList 的 modCount 值加 1,然后 Itr 类中的 expectedModCount 保持不变,所以会抛出异常。
同理可得,由于 add 操作也会导致 modCount 自增,所以不允许在 foreach 中增删改 ArrayList 中的元素。对此,推荐使用迭代器 Iterator 删除元素:
Iterator<String> iterator = arrayList2.iterator();
while(iterator.hasNext()){
String item = iterator.next();
if("1".equals(item)){
iterator.remove();
}
}
如果存在并发操作,还需要对 Iterator 进行加锁操作。
网友评论