集合可以说是平时开发用到最多的,今天就记录一下操作集合的时候报了一个ConcurrentModificationException异常,就这个问题来分析原因,避免后面同学入坑,以下是本文目录大纲:
1.复现异常出现的情景 (jdk 1.8 )
2.跟踪异常信息追踪报错逻辑
3.源码分析以及解决方案
若有不正,多多见谅
若有雷同,算我抄你
欢迎批评指正 ,转载请标明原文链接
复现ConcurrentModificationException异常的出现
public class ExceptionTest {
public static void main(String[] args) {
ArrayList<Object> list = Lists.newArrayList();
list.add("1");
Iterator<Object> iterator = list.iterator();
while(iterator.hasNext()){
Object s = iterator.next();
//System.out.println(s);
if(s.equals("1")){
list.remove(s);
}
}
}
}
运行结果:
ConcurrentModificationException ,内心震惊!这个应该没问题吧,又运行一遍,还是这
根据异常信息追踪报错逻辑
还是老规矩(一定要看代码中的注释,把排查思路搞清楚,然后再自己走一遍),看异常信息从下往上看着走,直接定位到ArrayList$Itr.next(ArrayList.java:857)点击进去看代码如下:找到next方法的调用处,
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
// 下一个返回元素的索引,也就是游标,用来判断是否还有元素
int cursor;
// 返回最后一个元素的索引,如果没有元素了,就返回-1
int lastRet = -1;
//这个是关键的点,把modCount值给到expectedModCount
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();
}
}
那就看一下这两个变量到底是什么?首先看看modCount,原来是抽象类里面的一个成员变量,上面的注释一定要回头好好看看,很重要,意思在此先不翻译,逻辑优先
// 这个值在ArrayList中执行add方法的时候,会自增
protected transient int modCount = 0;
再去找expectedModCount ,点进去之后来到了ArrayList的里面,也就是上面那个代码的这里 int expectedModCount = modCount;
所以到这里我们就清楚了报错的原因,那么这两个值怎么不相等呢,我怎么判断呢,把最初的那个注释代码去掉,看结果如下:
结果打印出来了,那就是说在第二次循环的时候,才报错的,这之间我只是做了一个删除操作,所以就是这个删除操作引发的血案,聚焦到这里,看删除是怎么操作的,继续看源码:
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 代码走到这里,
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
// 调用这个方法删除
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
// modCount 先自增一下
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
删除之前modCount++了,这里我们需要注意一下,列出几个值
第一次循环之前:
cursor = 0;
lastRet = -1;
modCount = 1;(因为list有一个add操作);
expectedModCount = modCount = 1;
size = 1;
第一次循环结束之后,也就是删除执行完:
cursor=1;
lastRet =0;
modCount =2;
expectedModCount =1;
size = 0;
所以第二次调用iterator.hasNext()的时候,判断cursor != size 肯定为true,
接着往下走再去调用Iterator.next()的时候,实际就是调用checkForComodification();这个方法,再次比较两个值的,结果肯定不相等啦,所以就抛出java.util.ConcurrentModificationException啦!
这里呢,就多追溯一点,这个Iterator,有的同学好像不是很清楚,我们为什么从集合里面可以直接拿到迭代器呢?就好比上面的那个list.iterator(),集合里面我们知道list和set都是接口Collection的实现类,我们去看Collection这个接口就知道它是继承Iterable这个接口的,所以拿到迭代器就明了了。
分析完报错原因就清晰明了怎么解决了,根本就问题在于,调用list.remove()方法导致modCount和expectedModCount的值不一致。
注意啦,像使用for-each进行迭代实际上也会出现这种问题在调用删除的方法的时候,for-each再针对集合的循环实际底层就是迭代器,针对数组是另外一种情况,这个本文暂不详细诉说
解决方案
删除操作不用list的remove,而是用迭代器中的remove方法
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();
}
}
其实就是多了一步赋值操作expectedModCount = modCount
来重新跑下,看下结果
public class ExceptionTest {
public static void main(String[] args) {
ArrayList<Object> list = Lists.newArrayList();
list.add("1");
Iterator<Object> iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(list.size());
Object s = iterator.next();
System.out.println(s);
if(s.equals("1")){
// list.remove(s);
iterator.remove(); //就是这行代码
}
}
}
}
运行结果:
完全正确!本文讲述的都是单线程情况下,多线程情况下,还没有遇到过,也没有测试过,不过网上应该有很多的文章参考。
思考:
试想一下如果代码换成这样去执行,结果是怎么样呢?自己动手试一下吧
public class ExceptionTest {
public static void main(String[] args) {
ArrayList<Object> list = Lists.newArrayList();
list.add("1");
list.add("2");
Iterator<Object> iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(list.size());
Object s = iterator.next();
System.out.println(s);
if(s.equals("1")){
list.remove(s);
// iterator.remove();
}
}
}
}
欢迎在评论区分享你的研究成果,一起学习进步喽!