Java集合 ArrayList 多线程下报错ArrayIndexOutOfBoundsException

问题描述

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);
}

问题出在以下几点:

  1. ensureCapacityInternal()多线程同时执行到这里判断size都符合不扩容条件

    多个线程在同时走到代码行4时,都判断当前数组容量够,然后都尝试写入,造成数据覆盖或越界。

  2. 多个线程同时修改 size,导致写入同一个位置

    size += numNew; 不是原子操作,会出现数据覆盖或 size 失效,进而造成越界。

  3. System.arraycopy 写入的范围不正确

    线程 A 和线程 B 在同一时间调用 addAll(),都以旧的 size 为基础写入,会出现错位或越界。

正确做法

  1. 使用CopyOnWriteArrayList
  2. Collections.synchronizedList()
相关推荐
古法安卓5 小时前
Android-日志系统源码解析
android·java·android studio
小夏coding6 小时前
从"一把梭"到"精妙拆解" —— 滑动窗口计时框架的设计演进
java·后端
MacroZheng7 小时前
同事问我:"Claude Code经常失忆,不怕它把项目搞炸?",我:"怕,三个Markdown文件给它装个永不丢失的外置大脑!"
java·人工智能·后端
十年Java程序媛8 小时前
深度实战:JDK21 虚拟线程 + HikariCP 生产正确配比、坑点、监控全套
java·spring boot
xiaoqiMikko8 小时前
有人在搜一个不存在的 Tomcat 版本
java·tomcat
用户31268748772012 小时前
ConcurrentHashMap 怎么保证线程安全?从分段锁到 CAS+synchronized
java
vipxieliang12 小时前
ValidX vs Apache Commons Validator:功能与性能对比
java·spring boot
SimonKing12 小时前
升级Spring Boot 4后,从 Jackson 2 到 3,到底有哪些变化
java·后端·程序员
吃饱了得干活13 小时前
一篇讲清楚Spring Boot:自动装配、启动器、过滤器、拦截器、设计模式
java·spring boot·后端
lhldsg14 小时前
AI零售系统实战指南:从架构设计到落地部署全解析
java·人工智能·小程序·uni-app·零售