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

相关推荐
程序猿麦小七4 分钟前
基于springboot的景区网页设计与实现
java·spring boot·后端·旅游·景区
蓝田~12 分钟前
SpringBoot-自定义注解,拦截器
java·spring boot·后端
theLuckyLong13 分钟前
SpringBoot后端解决跨域问题
spring boot·后端·python
.生产的驴14 分钟前
SpringCloud Gateway网关路由配置 接口统一 登录验证 权限校验 路由属性
java·spring boot·后端·spring·spring cloud·gateway·rabbitmq
小扳18 分钟前
Docker 篇-Docker 详细安装、了解和使用 Docker 核心功能(数据卷、自定义镜像 Dockerfile、网络)
运维·spring boot·后端·mysql·spring cloud·docker·容器
v'sir28 分钟前
POI word转pdf乱码问题处理
java·spring boot·后端·pdf·word
李少兄32 分钟前
解决Spring Boot整合Redis时的连接问题
spring boot·redis·后端
码上一元5 小时前
SpringBoot自动装配原理解析
java·spring boot·后端
枫叶_v7 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
杜杜的man8 小时前
【go从零单排】Closing Channels通道关闭、Range over Channels
开发语言·后端·golang