多线程导致的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,因为它会在每次写入操作时创建一个新的副本,这可能会导致性能问题和不断增长的内存占用,在多线程情况下可能会导致死锁。

相关推荐
Dajiaonew14 分钟前
SpringCloud Stream 快速入门
后端·spring·spring cloud
用户693717500138425 分钟前
8.Kotlin 类:类的基础:主构造函数与次构造函数
android·后端·kotlin
用户693717500138426 分钟前
9.Kotlin 类:类的核心:属性 (Property) 与自定义访问器 (Getter/Setter)
android·后端·kotlin
回家路上绕了弯31 分钟前
接口 QPS 从 100 飙到 1000?从应急到根治的全流程优化方案
分布式·后端
The_cute_cat43 分钟前
通过内网穿透为课设临时添加域名访问【springboot+Vue】
vue.js·spring boot·后端
howcode1 小时前
女友去玩,竟带回一道 “虐哭程序员” 的难题
后端·设计模式·程序员
z***94841 小时前
SpringBoot教程(三十二) SpringBoot集成Skywalking链路跟踪
spring boot·后端·skywalking
程序员西西1 小时前
SpringBoot 数据存储实战拆解!
后端
辜月十1 小时前
NSSM服务
后端
00后程序员2 小时前
Objective-C 测试(OC 测试)指南 从单元测试到性能调优的多工具协同方法
后端