ArrayList 并发修改异常?

背景

如下代码

java 复制代码
public static void main(String[] args) {
    ArrayList<String> l = new ArrayList<>();
    l.add("1");
    for (String s : l) {
        l.remove(s);
    }
}

运行代码会报以下错误:

问题:在for循环或forEach中,对ArrayList进行增删操作,会报错ConcurrentModificationException,而倘若使用Iterator进行增删,则不会报错,究竟是何缘故?跟着Arvin来研究一下

原因

根据之前在《代码大全》和《程序员修炼之道:通向务实的最高境界》两书中,均有关于调试的技巧,先冷静,再仔细看报错的内容,其实可以看到这里的堆栈很浅,只有简单的几行调用,那么此时看一下是什么地方抛出的该异常

这里可以看到,,同时在ArrayList中调用iterator()方法时,会返回一个Itr对象,而Itr类实现了迭代器接口,而从上面的堆栈可以看出,是在调用迭代器的next()方法时,方法内部调用checkForComodification()方法时报出的该错误

这个方法里面,会去检查modCountexpectedModCount的值是否一致,modCount顾名思义,就是修改次数,该字段是ArrayList的父类AbstractList的字段

java 复制代码
/**
 * The number of times this list has been <i>structurally modified</i>.
 * Structural modifications are those that change the size of the
 * list, or otherwise perturb it in such a fashion that iterations in
 * progress may yield incorrect results.
 *
 * <p>This field is used by ...
 */
protected transient int modCount = 0;

这个上面说了一大段话都不重要,简单理解就是对里面元素进行增删时,这个参数值会发生变化

那到了这里,就可以看一下在for循环中调用的remove()方法了,

java 复制代码
public boolean remove(Object o) {
    final Object[] es = elementData;
    final int size = this.size;
    int i = 0;
    found: {
        if (o == null) {
            for (; i < size; i++)
                if (es[i] == null)
                    break found;
        } else {
            for (; i < size; i++)
                if (o.equals(es[i]))
                    break found;
        }
        return false;
    }
    fastRemove(es, i);
    return true;
}

这里面逻辑不走赘述,就只是判断要删除的元素如果是null,就找到第一个null元素进行删除,否则就找到相同的元素进行删除,之后再执行fastRomve()方法,那再来看这个方法

java 复制代码
private void fastRemove(Object[] es, int i) {
    modCount++;
    final int newSize;
    if ((newSize = size - 1) > i)
        System.*arraycopy*(es, i + 1, es, i, newSize - i);
    es[size = newSize] = null;
}

这里多的也不需要看,只需关注modCount++这个操作,删除之前,对修改次数进行了变更,而上面next()方法中调用checkForComodification()方法,会去检查modCountexpectedModCount的值是否一致,很明显是完全不一致的,所以就报了这个错

综上,报错的原因,也就找到了

解决方案

  • 如果遍历删除元素,采用迭代器的方式去删除,具体原因,浅析一下迭代器中删除的代码
java 复制代码
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();
    }
}

这段代码也很简单,只需关注两个点,一个是在这里面仍然去调用了ArrayListremove()方法,另外,就是expectedModCount = modCount;操作,这样,再去迭代的时候,就不会报错两个值不一致了

  • 假如只是普通的元素删除,无需遍历,仍然可以使用ArrayListremove()方法
  • 选用其他的数据结构,例如CopyOnWriteArrayList,但需注意,一定要自己去阅读一下源码,比如这个里面,就不支持在迭代器中删除元素
java 复制代码
/**
 * Not supported. Always throws UnsupportedOperationException.
 * @throws UnsupportedOperationException always; {@code remove}
 *         is not supported by this iterator.
 */
public void remove() {
    throw new UnsupportedOperationException();
}
相关推荐
小毛驴85037 分钟前
在Spring Boot开发中,HEAD、OPTIONS和 TRACE这些HTTP方法各有其特定的应用场景和实现方式
spring boot·后端·http
梵得儿SHI40 分钟前
Java 反射机制核心类详解:Class、Constructor、Method、Field
java·开发语言·反射·class·constructor·java反射·java反射机制
m0_736927041 小时前
想抓PostgreSQL里的慢SQL?pg_stat_statements基础黑匣子和pg_stat_monitor时间窗,谁能帮你更准揪出性能小偷?
java·数据库·sql·postgresql
Jabes.yang1 小时前
Java面试大作战:从缓存技术到音视频场景的探讨
java·spring boot·redis·缓存·kafka·spring security·oauth2
zl9798991 小时前
SpringBoot-依赖管理和自动配置
spring boot·后端·状态模式
JaguarJack1 小时前
PHP8.5 的新 URI 扩展
后端·php
绝无仅有1 小时前
面试真实经历某商银行大厂数据库MYSQL问题和答案总结(一)
后端·面试·github
绝无仅有1 小时前
Docker 实战经验之关键文件误删恢复指南
后端·面试·github
Query*1 小时前
Java 设计模式——适配器模式进阶:原理深挖、框架应用与实战扩展
java·设计模式·适配器模式
Sirens.1 小时前
Java核心概念:抽象类、接口、Object类深度剖析
java·开发语言·github