如果在使用iterator(或者foreach,它实质上就是iterator)遍历元素时通过源列表直接删除元素会导致ConcurrentModificationException,jdk的设计者本意是fast-fail
1.用来对出现并发问题时提前报错,防止问题扩散难以查找原因
2.禁止在遍历时在外部修改源集合导致数据不一致问题,这个是错误使用
HashMap中关于ConcurrentModificationException的说明
The iterators returned by all of this class's "collection view methods" are fail-fast: if the map is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove method, the iterator will throw a {@link ConcurrentModificationException}. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.
Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.
下面这个就是2的具体表现
public class ConcurrentModifyExcetionTest {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
Iterator<Integer> itr = list.iterator();
while (itr.hasNext()) {
Integer element = itr.next();
list.remove(element);
}
}
}
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.github.alonwang.other.ConcurrentModifyExcetionTest.main(ConcurrentModifyExcetionTest.java:18)
下面通过源码分析
list.iterator(),可以看到返回了AbstractList的一个内部类Itr(PS.这里有个面向对象的常识,如果本类中找不到对应的方法,就去父类/接口中找)
public Iterator<E> iterator() {
return new Itr();
}
Itr,它是迭代器的一个内部实现,它最重要的字段就是expectedModCount
- expectedModCount 预期被修改的次数,属于Itr
私有,初始时和modCount相等 - modCount 集合被结构性修改(新增或删除)的次数,它是属于集合的
使用itr进行遍历/删除时都会进行checkForComodification()检查,只有在使用itr的remove()来移除元素,expectedModCount才会被更新,如果通过源集合直接删除,modCount会更新expectedModCount不会发生变化,也就导致modCount!=expectedModCount进而报错
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
//重点关注这里
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
对于单线程程序,正确的用法是这样的
public class ConcurrentModifyExcetionTest {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
Iterator<Integer> itr = list.iterator();
while (itr.hasNext()) {
Integer element = itr.next();
//使用itr的remove方法来移除
itr.remove();
}
}
}
这有引出另外一个问题,多线程下使用itr.remove()还会出现ConcurrentModificationException吗?
答案是 会的.问题的核心在于itr是线程私有的,这隐含着expectedModCount也是线程私有的.而modCount是线程共享的.
如果有一个线程对集合进行了结构性修改,那么modCount和此线程的expectedModCount会更新,其他线程的expectedModCount都不会更新,也就势必导致其他线程的expectedModCount!=modCount,最终导致ConcurrentModificationException
总结
ConcurrentModificationException是集合对并发的一种自我防御机制,它通过预先检查提前报错来防止更严重的问题,如果遇到这个问题要思考一下原因
- 自己使用错误
- 需要换用线程安全的集合如CopyOnWriteArrayList