ArrayList哪几种情况下会报java.util.ConcurrentModificationException吗?

ArrayList是一个很常用的集合类,底层是一个数组,只不过ArrayList封装了数组,实现了一些好用的方法例如add()方法,size()方法,get()方法等一系列的方法,并且实现了动态的扩容数组。
new ArrayList();创建了一个空数组,那么它的容量起始为0,为什么面试过程中很多人都说ArrayList的初始容量为10呢?原因在于当你第一次调用add()方法添加元素的时候会进行一次扩容。这时候就会扩容到默认的初始容量10
在ArrayList中定义了一个常量DEFAULT_CAPACITY,如下:
在这里插入图片描述
关于ArrayList的源码解读可以看我之前写的博客:
一文搞定ArrayList、LinkedList、HashMap、HashSet -----源码解读之ArrayList

本篇文章主要讲解并发修改异常出现的情况,也就是调用多线程并发调用add()方法

下面有一段测试代码

ArrayList<String> arrayList = new ArrayList();
for (int i = 0; i < 2000; i++) {//如果没有出现并发修改异常则讲循环调大点
    int finalI = i;
    new Thread(() -> {
        arrayList.add("" + finalI);
    }, "thread:" + i).start();

}

arrayList.stream().forEach(System.out::println);//遍历ArrayList进行打印

下面是执行代码的输出情况
在这里插入图片描述

你是否有疑惑,为什么会出现并发修改异常?这个异常是怎么抛出来的?

解开疑惑

下面跟我一起追踪源码,找到这个异常出现的位置。

我是这样做的,在add()方法上添加一个断点,进行debug调试。

会执行下面的代码

public boolean add(E e) {
    modCount++; //注意这个参数,是来自父类的成员变量 AbstractList
    add(e, elementData, size);//近一步调用三个参数的add方法
    return true;//表示添加成功
}
private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length) //判断是否需要扩容
        elementData = grow();//进行扩容,将扩容后的新数组重新赋值给 elementData
    elementData[s] = e; //将元素添加进数组中
    size = s + 1; // s是下标,size是实际的元素个数,因此需要 数组下标 +1 = 元素个数
}

会发现add方法似乎并没有关于java.util.ConcurrentModificationException异常的任何信息。
只是简单的将元素添加进去,如果长度不够就进行grow()扩容。
那会不会是这个grow()方法里面抛的异常?进一步追踪到grow()

private Object[] grow() {
    return grow(size + 1); //调用了带参数的 grow方法
}

会发现这里的也没有关于并发修改异常的代码

private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity >> 1           /* preferred growth */);
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } else {
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

调用了Arrays的copyOf()方法对元素进行拷贝,获得一个新的数组并返回
这里也没有并发修改异常的代码。那么到底是哪里抛的异常呢?

public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

异常并非是add()方法中抛出的异常,而是后面遍历打印抛出来的异常
进入遍历,当然测试代码中用的是java8的流操作打印的。
前面遗留了一个成员变量modCount++这个成员变量进行自增,这个变量到底来自哪里呢,追踪一下,来到了父类AbstarctList,从名称也可以知道这个成员变量是用来记录修改计数的。
在这个类搜索一下ConcurrentModificationException发现有11个地方有引用到(包括了注释里面的)。一个一个去看,找到了第一个地方引用的地方在内部类Itr中有引用,这个类之前分析过,是用来迭代遍历集合用的。换句话说就是在迭代遍历的时候检测到modCount与预期的值不同,然后抛出的这个异常。
造成这个结果的原因是多线程调用add()这个modCount会进行自增。
然后线程还没有处理完,后面又调用了迭代,进行获取元素,发现这个modCount计数和预期的expectedModCount不等,因此抛异常

下面是Itr类中定义的方法,以及抛异常的条件
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

在这个Itr内部类中有两个地方调用了这个方法,分别是
remove()
next()
这也就是说明,这两种方法可能会报java.util.ConcurrentModificationException

除了Itr这个类中有ConcurrentModificationException,内部类ListItr也有调用分别是
previous()
set(E e)
add(E e)
内部类RandomAccessSpliterator
定义了下面的检测方法

static void checkAbstractListModCount(AbstractList<?> alist, int expectedModCount) {
    if (alist != null && alist.modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

调用的方法有
tryAdvance(Consumer<? super E> action)
forEachRemaining(Consumer<? super E> action)
以及直接抛异常的
<E> E get(List<E> list, int i)方法
最后一个可能抛异常的地方是静态内部类SubList<E>中的
定义了检测并发修改异常的放法

private void checkForComodification() {
    if (root.modCount != this.modCount)
        throw new ConcurrentModificationException();
}


E set(int index, E element)
E get(int index)
int size()
void add(int index, E element)
E remove(int index)
oid removeRange(int fromIndex, int toIndex)
addAll(int index, Collection<? extends E> c)
ListIterator<E> listIterator(int index)
都有调用上面的checkForComodification()都有可能抛并发修改异常。
换句话说,如果调用了这些方法都有可能抛出java.util.ConcurrentModificationException异常

而在测试代码中之所以抛出java.util.ConcurrentModificationException异常的原因是迭代获取元素调用了ArrayList中的forEach方法
传入的是一个消费性接口

 @Override
 public void forEach(Consumer<? super E> action) {
     Objects.requireNonNull(action);
     final int expectedModCount = modCount;
     final Object[] es = elementData;
     final int size = this.size;
     for (int i = 0; modCount == expectedModCount && i < size; i++)
         action.accept(elementAt(es, i));
     if (modCount != expectedModCount)
         throw new ConcurrentModificationException();
 }

由于modCount != expectedModCount在后面的if判断条件中符合条件因此抛出并发修改异常
这是测试案例中通过流的forEach导致并发修改异常的原因。

总结:

ArrayList中的add(E e)方法会导致并发问题,但并不会报java.util.ConcurrentModificationException异常,而是在进行遍历的时候。例如测试代码中的forEach中抛异常。并非并发异常一定会出现,但是ArrayList的确会出现并发问题。

也就是说,不管你用多少个线程进行多少次add(E e)都不会抛异常,虽然的确是出现了重复写入同一个数组中的位置,但是并不能知道是那个线程导致的并发异常

  • 8
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

诗水人间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值