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)
都不会抛异常,虽然的确是出现了重复写入同一个数组中的位置,但是并不能知道是那个线程导致的并发异常