并发修改异常探究

并发修改异常

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

posted @ 2021-08-10 16:03  灰线  阅读(408)  评论(0)    收藏  举报