并发修改异常探究
并发修改异常
1 什么时候会发生异常
并发就是同一时刻发生,并发修改的意思就是同一时刻发生并修改。当方法检测到对象的并发修改,但不允许这种修改时,会抛出此异常。
最常见的出现兵法修改异常的场景:当我们在对集合进行迭代操作的时候,如果同时对集合对象中的元素进行某些修改操作,就会导致并发修改异常的产生。
对于以下代码
- 在一个存储字符串的集合中,如果存在字符串"java",则再添加一个"world"
public class Exercise {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("java");
list.add("study");
list.add("hello");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
if("java".equals(next)){
list.add("world");
}
}
}
}
控制台输出
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
at com.study.chapter14.exceptionStudy.Exercise.main(Exercise.java:14)
- ConcurrentModificationException就是并发修改异常
2 异常产生原因
2.1 根据源码寻找线索
- 通过控制台信息追寻源码
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
异常出现的位置在ArrayList类内部类Itr中的checkForComodification方法
- 此处源码
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
- 当一个名为modCount的变量值不等于 expectedModCount的变量值时,异常会被抛出
2.2 探究这两个变量代表什么
- modCount
modCount是定义在AbstractList抽象类中public修饰的成员变量,ArrayList是此类的子类,从AbstractList那里继承到了modCount这个变量
源码对modCount的解释为:这个变量其实就代表了集合在结构上被修改的次数
- expectedModCount
expectedModCount是内部类Itr中的成员变量,当ArrayList对象调用iterator方法时,会创建内部类Itr的对象,并给其成员变量expectedModCount赋值为ArrayList对象成员变量的值modCount
源码为
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
- 深究modCount改变原因
当我们创建ArrayList对象的时候,ArrayList对象包含了此变量modCount并且初始化为0
通过源码可知,ArrayList中能改变modCount的方法都是添加元素的相关功能和删除元素的相关功能
每删除一个元素,modCount的值会自增一次
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
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
return oldValue;
}
我们每次进行对集合中元素个数变化的操作时,modCount的值就会+1。也就是说,增删会修改modCount值,改查不会影响modCount
modCount就记录了对集合元素个数的改变次数
2.3 分析迭代器为何会抛出异常
2.3.1 迭代器的创建
当ArrayList对象调用iterator方法时,会创建内部类Itr的对象,此时迭代器对象中有两个最关键的成员变量:cursor、expectedModCount
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
探究这两个变量的作用
- cursor
迭代器的工作就是将集合中的元素逐个取出,而cursor就是迭代器中用于指向集合中某个元素的指针
在迭代器迭代的过程中,cursor初始值为0,每次取出一个元素,cursor的值会+1,以便下一次能指向下一个元素,知道cursor值等于集合的长度为止,从而达到取出所有元素的效果
- expectedModCount
expectedModCount在迭代器对象创建时被赋值为modCount
当迭代器创建完成之后,如果我们没有对集合进行增删操作,expectedModCount的值是会等于modCount的值的
在迭代集合元素的过程中,迭代器通过检查expectedModCount和modCount的值是否相同,以防出现并发修改
2.3.2 迭代器迭代过程源码解析
-
在我们使用迭代器的时候,一般会调用迭代器的hasNext方法判断是否还有下一个元素。源码为
public boolean hasNext() { return cursor != size; }- cursor初始值为0,默认指向集合中第一个元素,每次取出一个元素,cursor会自增一次
- size是集合中的成员变量,用于表示集合的元素个数
- 集合的最后一个元素的索引为size-1,只要cursor的值不等于size,就证明存在下一个元素,将其返回。如果cursor等于size,说明迭代完最后一个元素,没有下一个元素了。
-
当我们通过迭代器的hasNext方法返回true,确定集合还有元素时,通常我们会通过迭代器的另一个方法next取出次元素。源码为
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]; }- next方法的第一行就是调用checkForComodification方法,产生并发修改异常的地方。
- 迭代器每一次取出元素前都会检查集合中的modCount和最初赋值给迭代器的expectedModCount是否相等,如果不等,说明产生了增删操作,modCount的值被改变了。抛出并发修改异常
- 如果没有异常产生,next方法最后一行会返回cursor指向的元素
3 并发修改的意义及异常解决方案
3.1 这个异常对程序有什么意义
- 迭代器是通过cursor指针指向对应集合元素来挨个获取集合中元素的,每次获取对应元素后cursor值+1,指向下一个元素,直到集合最后一个元素。
- 如果在迭代器获取元素的过程中,集合中元素的个数突然改变,那么下一次获取元素时,cursor能否正常的指向集合的下一个元素就变得未知了,这种不确定性有可能导致迭代器工作出现意想不到的问题
- 为了防止在将来某个时间任意发生不确定行为的风险,我们在使用迭代器的过程中不允许修改集合元素的结构(即不允许修改元素个数),否则迭代器会抛出异常结束程序
3.2 如果遇到需要在遍历集合的同时修改集合结构的需求该如何处理
3.2.1 迭代器实现增删
在迭代器迭代的过程中,我们虽然不能通过集合直接增删元素,但是其实迭代器中是有这样的方法可以实现增删的
-
通过ArrayList中iterator方法返回的Itr迭代器对象包含有一个remove方法
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(); } } -
通过Itr迭代器的子类对象ListItr中有添加元素的add方法
public void add(E e) { checkForComodification(); try { int i = cursor; ArrayList.this.add(i, e); cursor = i + 1; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
以上两个方法在增删完元素后都对指针cursor进行了相应的处理,避免了出现迭代器获取元素的不确定行为
3.2.2 更换遍历集合方式
异常是迭代器抛出的,我们除了可以使用迭代器遍历集合,还可以使用其他方法
- 属于List体系的集合我们可以使用普通for循环,通过索引获取集合元素的方法来遍历集合,这个时候修改集合结构是不会出现异常的
- 不属于List体系的集合,我们可以通过单列集合顶层接口Collection中定义的toArray方法将集合转为数组,这个时候就不需要担心出现并发修改异常了
4 其他相关问题
4.1 增强for循环和迭代器
foreach循环也就是我们常说的增强for循环,其实foreach循环的底层是用迭代器实现的
所以我们不能在foreach中对集合结构进行修改,否则可能会出现并发修改异常
4.2 迭代器修改集合结构的特殊情况
当迭代至集合倒数第二个元素的同时,删除集合元素不会导致并发修改异常
public class Exercise {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("java");
list.add("study");
list.add("hello");
list.add("world");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
if("hello".equals(next)){
list.remove("java");
}
}
}
}
上面代码在迭代到倒数第二个元素"hello"的时候,删除了"java",但是并没有出现并发修改异常。如果换成"study",会继续出现异常
- 原因解释
- 集合中倒数第二个元素的索引为size - 2,当迭代器取出集合倒数第二个元素的时候cursor指向的位置会向右移动一位,值会变成size - 1。
- 如果此时通过集合去删除一个元素,集合中元素个数会减一,所以size值会变成size - 1
- 当迭代器试图去获取最后一个元素的时候,会先判断是否还有元素,调用hasNext方法,返回cursor != size,但是此时的cursor和此时的size的值都等于删除之前的size - 1,两者相等,那么hasNext方法就会返回false,迭代器就不会再调用next方法获取元素了。
参考文章作者:魔数师
参考文章地址:https://blog.csdn.net/qq_29534705/article/details/80899351

浙公网安备 33010602011771号