问题描述
java ArrayList多线程下java.util.concurrent.CompletionException: java.lang.ArrayIndexOutOfBoundsException异常
ArrayList 报错 java.lang.ArrayIndexOutOfBoundsException
:如果是在多线程场景下进行操作,基本上是 线程安全问题导致。
ArrayList 是 非线程安全 的,在并发读写时,是多个线程同时 add/remove/get 元素时,可能导致内部结构(尤其是 elementData[] 数组)混乱,抛出 ArrayIndexOutOfBoundsException 异常。
case代码
我遇到的case,是多线程在操作list.addAll()
java
SyncTask<UserDataLog> syncTask = new SyncTask<>(executorService);
List<User> userList = get();
List<List<User>> taskList = Lists.partition(userList, 10);
syncTask.addTask(
taskList,
tasks -> {
tasks.forEach(
user -> {
List<UserDataLog> logList = checkUserDataByOne(operator, user);
if (CollectionUtils.isNotEmpty(logList)) {
returnPos.addAll(logList);
}
});
});
syncTask.sync();
源码分析
ArrayList 底层是数组 Object[] elementData。在 add() 时会涉及两个非原子操作:
java
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
// 判断数组容量是否足够,如果不足,进行扩容
ensureCapacityInternal(size + numNew);
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
问题出在以下几点:
-
ensureCapacityInternal()多线程同时执行到这里判断size都符合不扩容条件
多个线程在同时走到代码行4时,都判断当前数组容量够,然后都尝试写入,造成数据覆盖或越界。
-
多个线程同时修改 size,导致写入同一个位置
size += numNew; 不是原子操作,会出现数据覆盖或 size 失效,进而造成越界。
-
System.arraycopy 写入的范围不正确
线程 A 和线程 B 在同一时间调用 addAll(),都以旧的 size 为基础写入,会出现错位或越界。
正确做法
- 使用CopyOnWriteArrayList
- Collections.synchronizedList()