ConcurrentModificationException位于java.util包下,中文意思就是并发修改异常。
当一个线程在遍历集合时,另一个线程对集合进行修改(添加或删除元素),此时遍历过程可能会出现问题。为了避免这类问题,某些迭代器在检测到这种情况时会抛出ConcurrentModificationException,这些迭代器就叫做fail-fast(快速失败)迭代器。
注意:此异常并不总是表示对象被不同的线程同时修改。 如果单个线程使用违反对象合同的方法调用集合,也可能会抛出此异常。
例如,如果线程在使用快速失败迭代器迭代集合时直接修改集合,则迭代器将抛出此异常。
单线程抛ConcurrentModificationException分析
首先演示单线程中使用集合抛出ConcurrentModificationException的情况:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("2".equals(item)) {
list.remove(item);
}
}
执行一下:
发现在String item = iterator.next();这里抛出了异常,那么迭代器的next方法里到底做了什么?
因为是创建ArrayList的迭代器,所以找到它的迭代器代码:
cursor代表集合中下一个访问的元素的下标,
lastRet代表集合中上一个访问的一个元素的下标。
expectedModCount代表集合期望的修改次数,初始值为modCount。
elementData是ArrayList底层维护的数组对象。
modCount代表集合实际被修改的次数,初始值为0。这个变量是在AbstractList中定义的。
看看checkForComodification方法:
该方法会判断modCount和expectedModCount是否相等,如果不相等就抛出异常。
然后从next()源码中能看到执行完checkForComodification()之后,又再一次进行容错保护:
当下一个访问的元素超出了数组的范围时,会抛出ConcurrentModificationException。
对modCount值的修改肯定实在remove方法中做的:
每当从要删除一个元素时,都会遍历集合找到要删除的元素的下标,然后调用fastRemove方法,和fail-fast呼应上了。
终于找到了,就是这里修改的modCount。修改完modCount值后,会判断集合删除一个元素后是否还有元素,如果还有就把要删除元素的后面的所有元素往前移动一个位置,将最后一个位置置为null方便GC。
阿里开发手册中集合遍历问题的思考
根据前面对迭代器源码的分析发现在遍历集合过程中删除元素会导致ConcurrentModificationException。那会不会有一种特殊情况呢?还真有,今天看了看阿里的开发手册中就提到了这个现象:
不信这个邪,跑代码试试:
移除元素不同结果居然不同,到底哪里出了问题,为什么移除“1”就不抛出异常了呢?
首先把条件整理一下:
- foreach底层还是走的迭代器,看下.class。
- list中有两个元素,依次是1,2。
- 添加完元素后modCount和expectedModCount的值都为2
那么执行完remove方法,将“1”从集合中移除后,modCount=3,expectedModCount=2。如果再执行next方法肯定会抛出ConcurrentModificationException。那么没有抛出异常只能是没有执行下一次循环。
看看判断条件hasNext():
hasNext方法中将cursor和size比较,如果cursor == size就说明集合中没有元素了。
嗯?好像找到原因了。
现在cursor = 1,而size也是1,正好骗过了迭代器。所以遍历过程中,使用集合的remove方法是很不安全的。
那么移除的元素是“2”的时候发生了什么呢?
移除2之后,cursor=2,size=1,cursor!=size,还会执行一次循环,进入next方法,所以抛出了异常。
解决方案
1.单线程情况,删除元素使用迭代器的remove方法。迭代器中的remove方法会更新expectedModCount的值。
2.多线程情况
- 仅remove元素,可以对Iterator对象加锁
- 如果想要进行add和remove,使用并发容器CopyOnWriteArrayList代替ArrayList