多线程导致的java.lang.ArrayIndexOutOfBoundsException

在初始化三方SDK时,一个 ArrayList<WebStateChangeListener> 出现如下堆栈:

csharp 复制代码
java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 0
  at java.base/java.util.ArrayList.add(ArrayList.java:455)
  at java.base/java.util.ArrayList.add(ArrayList.java:467)
  ...
  业务代码堆栈

问题偶现,在Andorid 14上奔溃3次,属于新版本出现的小概率偶现问题

对该问题思考过程以及排查

结论概述

【问题根本原因】:多线程(主线程和三方SDK-子线程)并发环境下 同时修改线程不安全的 ArrayList 导致数组越界异常

【解决】:对修改线程不安全的集合方法要加锁 或者 使用线程安全的集合

排查过程

对业务代码进行简化,Demo大致如下:

这个示例创建了100个线程,每个线程都尝试向ArrayList添加100000个元素。由于ArrayList不是线程安全的,多个线程可能会同时修改它,导致ArrayIndexOutOfBoundsException或其他异常。

arduino 复制代码
public class MultiModifyList {
​
   // 存放初始化三方SDK状态的Listener集合
    @Nullable
    private final List<String> listeners;
  
    public MultiModifyList(@Nullable List<String> list) {
        this.listeners = list;
    }
​
    // 模拟业务方法,遍历获取Listener的回调
    public void fakeMultiModify() {
        int numThreads = 100;
        List<Thread> threads = new ArrayList<>();
​
        for (int i = 0; i < numThreads; i++) {
            Thread thread = new Thread(() -> {
                for (int j = 0; j < 100000; j++) {
                    addElement("Element " + j);
                }
            });
            threads.add(thread);
            thread.start();
        }
​
        for (Thread thread : threads) {
            try {
                if (thread != null) {
                    thread.join();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
​
        System.out.println("ArrayList size: " + listeners.size());
​
    }
​
    // 对外提供添加Listene的API
    public void addElement(String value) {
        if (listeners != null) {
            listeners.add(value);
        }
    }
}

【首先】拿到以上堆栈,首先以上问题以及对应出问题的方法,查看循环中是否判断次数是否与 List.size y一致,但是由于是for-each循环,所以暂无数组越界产生的条件

【其次】根据堆栈查看 出问题的上下文 是否有remove操作,导致A线程在访问时,B线程将2元素删除;所以会导致访问的index为1,但是被删除后此时List的长度为0,属于 并发修改异常导致的问题(隐式问题)

可是,如上述Demo所示,上下文及相关类并未有对应的remove API或者相关操作,只有一个 add根据反馈的堆栈,出问题的只能是 addElement方法了。

但是, add(Element) 方法只是在尾部追加元素,并不是指定index添加元素;如果是指定index那肯定是有问题的。

然而,在多线程并发 场景下,依然不对;比如我有2个线程同时执行add(Element),他们在某一刻同时向同一index处添加,不还是并发修改导致的数组越界异常么


运行上述Demo,很容易复现这个问题,运行结果如下:

csharp 复制代码
Exception in thread "Thread-31" Exception in thread "Thread-24" java.lang.ArrayIndexOutOfBoundsException: Index 48310 out of bounds for length 47427
  at java.base/java.util.ArrayList.add(ArrayList.java:455)
  at java.base/java.util.ArrayList.add(ArrayList.java:467)
  at top.iqqcode.dailycase.MultiModifyList.addElement(MultiModifyList.java:47)
  at top.iqqcode.dailycase.MultiModifyList.lambda$fakeMultiModify$0(MultiModifyList.java:27)
  at java.base/java.lang.Thread.run(Thread.java:833)
Exception in thread "Thread-5" java.lang.ArrayIndexOutOfBoundsException: Index 48310 out of bounds for length 47427
  at java.base/java.util.ArrayList.add(ArrayList.java:455)
  at java.base/java.util.ArrayList.add(ArrayList.java:467)
  at top.iqqcode.dailycase.MultiModifyList.addElement(MultiModifyList.java:47)
  at top.iqqcode.dailycase.MultiModifyList.lambda$fakeMultiModify$0(MultiModifyList.java:27)
  at java.base/java.lang.Thread.run(Thread.java:833)
Exception in thread "Thread-39" java.lang.ArrayIndexOutOfBoundsException: Index 47483 out of bounds for length 47427
  ...

修正

  1. 最直观的是为 addElement 方法加把锁🔐,保证多线程执行时 是一个一个来的,保证可见性和一致性
  1. 或者换成线程安全的集合,如:

    • Collections.synchronizedList
    • ConcurrentLinkedQueue
arduino 复制代码
private final List<String> listeners = Collections.synchronizedList(new ArrayList<>());
​
private final ConcurrentLinkedQueue<String> listeners = new ConcurrentLinkedQueue<>();

该场景下,不推荐使用 CopyOnWriteArrayList,因为它会在每次写入操作时创建一个新的副本,这可能会导致性能问题和不断增长的内存占用,在多线程情况下可能会导致死锁。

相关推荐
Mr.45673 分钟前
Spring Boot 集成 PostgreSQL 表级备份与恢复实战
java·spring boot·后端·postgresql
LucianaiB3 分钟前
王炸组合!腾讯云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!
后端
白露与泡影7 分钟前
探索springboot程序打包docker的最佳方式
spring boot·后端·docker
开心就好20259 分钟前
本地执行 IPA 混淆 无需上传致云端且不修改工程的方案
后端·ios
架构师沉默22 分钟前
为什么一个视频能让全国人民同时秒开?
java·后端·架构
掘金码甲哥1 小时前
同样都是九年义务教育,他知道的AI算力科普好像比我多耶
后端
sthnyph1 小时前
SpringBoot Test详解
spring boot·后端·log4j
饼干哥哥2 小时前
搭建一个云端Skills系统,随时随地记录TikTok爆款
前端·后端
IT 行者2 小时前
LangChain4j 集成 Redis 向量存储:我踩过的坑和选型建议
java·人工智能·redis·后端
brucelee1862 小时前
Spring Boot 测试最佳实践
spring boot·后端·log4j