最近在回头打基础学习Java SE,研究List集合的过程中,遇到了ConcurrentModificationException 并发修改异常。在此记录下遇到问题的原因解析和解决方案。错误千奇百怪,解决问题的方法还是通用的,所以也算是记录下遇到问题该怎么自己去利用代码提示寻找原因的方法叭~
问题复现
最开始的需求是我需要遍历集合,并在指定位置添加元素
上来先丢出来错误示例代码:
package Gather;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* @author rob
* @title: ConcurrentModificationExceptionDemo
* @projectName java-learn
* @description: ConcurrentModificationException 并发修改异常
* @date 2021/11/1下午2:36
*/
public class ConcurrentModificationExceptionDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
list.add("java");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("world")) {
list.add("javaee");
}
}
System.out.println(list);
}
}
然后我们再来看一下报错提示内容
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1043)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:997)
at Gather.ConcurrentModificationExceptionDemo.main(ConcurrentModificationExceptionDemo.java:24)
报错解析
步入正题,我们先从最下面的报错提示开始看:
at Gather.ConcurrentModificationExceptionDemo.main(ConcurrentModificationExceptionDemo.java:24)
报错的是程序源代码的第24行,即:
String s = it.next();
就说明后面的next()
方法有问题,它不对劲(因为显然前面的创建String对象不可能报错呐 ),再看错误提示的上一行:
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:997)
说明程序还调用了java.util.ArrayList
的子类Itr的next()
方法(对的哦,$就是子类的意思 ),然后再往上看错误提示:
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1043)
说明程序还调用了java.util.ArrayList
的子类Itr的checkForComodification()
方法
好的,看完错误提示了,从上面的分析可以知道,错误的重点在next()
和checkForComodification()
方法上,接下来再回到程序中去找源码:
我们有一个List对象,且调用了add()
和iterator()
方法,所以我们先去java.util.List
去找这两个方法的源码( 如果你用的是IDEA等高级IDE,按住Ctrl再单击你需要查看的代码如现在的 ):List
就可以自动跳转至源码页了,没有就看我放出来的叭~
public interface List<E> {
boolean add(E var1);
Iterator<E> iterator();
}
发现这是一个接口,并没有实现具体的方法,所以我们再看程序源码 :
List<String> list = new ArrayList<>();
该List对象是通过ArrayList<>()
方法利用多态性new出来的,所以我们又需要去java.util.ArrayList
下去翻源码,由于它实现了List接口,所以需要找到复写的add()
和iterator()
方法,同时发现在iterator()
方法中返回了子类Itr
对象,从错误提示中得到这里只提取我们上面所提及到的next()
和checkForComodification()
方法(别的没用到所以就不用看了):
public class ArrayList<E> extends AbstractList<E> implements List<E> {
public boolean add(E e) {
++this.modCount;
this.add(e, this.elementData, this.size);
return true;
}
public Iterator<E> iterator() {
return new ArrayList.Itr();
}
private class Itr implements Iterator<E> {
int expectedModCount;
Itr() {
this.expectedModCount = ArrayList.this.modCount;
}
public E next() {
this.checkForComodification();
int i = this.cursor;
if (i >= ArrayList.this.size) {
throw new NoSuchElementException();
} else {
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
} else {
this.cursor = i + 1;
return elementData[this.lastRet = i];
}
}
}
final void checkForComodification() {
if (ArrayList.this.modCount != this.expectedModCount) {
throw new ConcurrentModificationException();
}
}
}
}
诶嘿,是不是看到了熟悉的ConcurrentModificationException?我们最终实在next()
方法出的异常,所以看Itr.next()
,第一行就调用了checkForComodification()
,而它只有一个if语句,所以报错抛出异常的一定是它。
什么时候才会抛出这个异常呢?if语句的条件是ArrayList.this.modCount != this.expectedModCount
,那么modCount和expectedModCount是什么呢,具体数值又是怎么来的,就是解决这个报错的关键了。
modCount:是实际修改集合的次数
expectedModCount:程序预期的修改集合的次数
从Itr的无参构造函数中我们可以看到:
Itr() {
this.expectedModCount = ArrayList.this.modCount;
}
啊哈,最开始他们是一致的,所以是我们在中间那一步操作导致的不一致引起的抛出ConcurrentModificationException 并发修改异常。
那modCount
这个变量来自哪里呢?
通过查看源码(就是在这个变量上按住Ctrl单击呐 ),得知它是来自ArrayList继承的父类AbstractList里:
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
protected transient int modCount = 0;
}
所以一开始,这两个值都是0
那问题就是哪一步使得modCount
改变了。查看程序源代码得知当程序出现"world"
时就会执行add()
方法:
public boolean add(E e) {
++this.modCount;
this.add(e, this.elementData, this.size);
return true;
}
++this.modCount;
使用前modCount
+1,所以成功调用add()
方法前:
modCount
=1;expectedModCount
=0;
1=0?不等于呀,所以程序肯定要抛出异常
原因
在通过迭代器遍历的时候,通过集合添加了元素,造成迭代器判断预期修改的次数和实际修改的次数不一致,这样就造成了并发修改异常。
解决方案
既然迭代器会出现问题那么用另一种循环不就好了,例如for循环格式:
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
if (s.equals("world")) {
list.add("JavaEE");
}
}
修改后完整的代码为:
package Gather;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* @author rob
* @title: ConcurrentModificationExceptionDemo
* @projectName java-learn
* @description: ConcurrentModificationException 并发修改异常
* @date 2021/11/1下午2:36
*/
public class ConcurrentModificationExceptionDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
list.add("java");
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
if (s.equals("world")) {
list.add("JavaEE");
}
}
System.out.println(list);
}
}
成功运行并在控制台答应输出结果为:
[hello, world, java, JavaEE]
这是因为修改后for循环遍历程序虽然也修改了modCount
,但是遍历的时候使用的是ArrayList.get()
方法,该方法仅通过索引获取并返回对应元素,并没有执行checkForComodification()
方法,也就不会进行判断:
public E get(int index) {
Objects.checkIndex(index, this.size);
return this.elementData(index);
}
好啦好啦,溜了溜了