关于JAVA并发修改异常
一、产生原因
当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常。迭代器底层在遍历集合时有一个指针,此时若是再对集合添加元素,相当于用集合原来的指针添加元素,然后就变成了两个指针操作同一个对象,就会发生并发修改异常。
二、常见场景
当我们在对集合进行迭代操作的时候,如果同时对集合对象中的元素进行某些操作,则容易导致并发修改异常的产生。比如说对一个集合使用迭代器遍历ArrayList的同时,又去使用这个ArrayList集合去添加元素,那么会发生并发修改异常
三、示例代码 (ArrayList)
public class Test {
public static void main(String[] args){
ArrayList<String> list = new ArrayList<String>();
list.add("Java");
list.add("Hello");
list.add("World");
//获取迭代器对象
Iterator<String> it = list.iterator();
//如果迭代器判断集合中还有下一个元素则继续循环
while(it.hasNext()){
//获取集合中迭代器所指向的元素
String str = it.next();
//如果这个元素内容是"Java"
if(str.equals("Java")) {
//则在集合中添加一个"Android"
list.add("Android");
}
}
}
}
上述代码会产生并发修改异常:ConcurrentModificationException。如下:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)
at java.util.ArrayList$Itr.next(ArrayList.java:831)
at com.itheima.day02.Test5.main(Test5.java:17)
四、异常产生根源分析
- 由控制台信息可知异常出现的位置出现在ArrayList类中内部类Itr中的checkForComodification方法,其源码为:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
由源码可知,modCount不等于expectedModCount时,抛出异常
-
modCount和expectedModCount是什么?
modCount是定义在AbstractList抽象类中的public修饰的成员变量,而ArrayList是此类的子类,那么代表ArrayList继承到了modCount这个变量。这个变量其实就代表了集合在结构上修改的次数。expectedModCount是内部类Itr中的成员变量,当ArrayList对象调用iteroter()方法时,会创建内部类Itr的对象,并给其成员变量expectedModCount赋值为ArrayList对象成员变量的值modCount。
itr部分源码:
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
int expectedModCount = modCount;
....
故当Itr对象被创建的时候,expectedModCount的值会等于modCount变量的值。
-
modCount变量的值如何被修改
查阅源码发现,创建ArrayList对象的时候,ArrayList对象里包含了此变量modCount并且初始化值为0;增删元素时modCount的值就会+1,修改元素值并不会影响modCount的值,也就是说modCount变量就是记录了对集合元素个数的改变次数。 -
结合迭代器工作流程进一步分析
当ArrayList对象调用iteroter()方法时,会创建内部类Itr的对象。此时迭代器对象中有两个最关键的成员变量:cursor、expectedModCount
cursor:
迭代器的工作就是将集合中的元素逐个取出,而cursor就是迭代器中用于指向集合中某个元素的指针在迭代器迭代的过程中,cursor初始值为0,每次取出一个元素,cursor值会+1,以便下一次能指向下一个元素,直到cursor值等于集合的长度为止,从而达到取出所有元素的效果。
expectedModCount:
expectedModCount在迭代器对象创建时被赋值为modCount,当创建完迭代器对象后,如果我们没有对集合结构进行修改,expectedModCount的值是会等于modCount的值的。在迭代集合元素的过程中,迭代器通过检查expectedModCount和modCount的值是否相同,以防止出现并发修改。使用迭代器时:
调用迭代器的hasNext()方法判断是否还有下一个元素,其原理如下:
cursor初始值是0,默认指向集合中第一个元素,每次取出一个元素,cursor值就会自增一次
size是集合中的成员变量,用于表示集合的元素个数
因为集合中最后一个元素的索引为size-1,只要cursor值不等于size那么就证明还有下一个元素,此时hasNext方法返回true,如若cursor值与size相等了,那么证明已经迭代完了最后一个元素,此方法返回false。通过迭代器的另一个方法next取出此元素,原理如下
next()方法第一行就是调用checkForComodification()方法,也就是我们上文中分析过并发修改异常出现根源
当迭代器通过next()方法返回元素之前都会检查集合中的modCount和最初赋值给迭代器的expectedModCount是否相等,如果不等,则抛出并发修改异常。
也就说,当迭代器工作的过程中,不允许集合擅自修改集合结构,如果修改了会导致modCount值变化,从而不会等于expectedModCount,那么迭代器就会抛出并发修改异常。
如果没有异常产生,next()方法最后一行会返回cursor指向的元素。
五、解决方案
- 使用迭代器遍历,使用迭代器添加元素!
使用while循环、hasNext()与next()方法遍历元素
使用ListIteractor接口中的add(object e)方法添加元素 - 使用集合遍历,使用集合添加元素!
使用for循环、size()方法与get()方法遍历元素
使用add()方法添加元素 - 特别的:当迭代至集合倒数第二个元素的同时,删除集合元素不会导致并发修改异常。
public static void main(String[] args){
ArrayList<String> list = new ArrayList<String>();
list.add("Java");
list.add("Hello");
list.add("World");
for (String s : list) {
if(s.equals("Hello")){
list.remove("Java");
}
}
//控制台输出:[Hello, World]
System.out.println(list);
}
- 原因解释:
集合中倒数第二个元素的索引为size - 2,当迭代器取出集合倒数第二个元素的时候,cursor指向的位置会向右移动一位,值会变为size - 1;
如果此时通过集合去删除一个元素,集合中元素个数会减一,所以size值会变为size - 1;
当迭代器试图去获取最后一个元素的时候,会先判断是否还有元素,调用hasNext()方法,上文中已经分析过,hasNext()方法会返回cursor!=size,但是此时的cursor和此时的size值都等于删除之前的size - 1,两者相等,那么hasNext()方法就会返回false,迭代器就不会再调用next方法获取元素了。
注明:此文章为转载
原作者:HUanLove
链接:https://www.jianshu.com/p/d1a384c58006