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

相关推荐
Asthenia041226 分钟前
从零开始:Dockerfile 编写与 Spring Cloud 项目部署到 Docker Compose
后端
Asthenia04121 小时前
准备面试:Jenkins部署SpringCloudAlibaba微服务商城全攻略
后端
woniu_maggie1 小时前
SAP EXCEL DOI 详解
开发语言·后端·excel
uhakadotcom2 小时前
云计算与开源工具:基础知识与实践
后端·面试·github
Asthenia04122 小时前
零基础指南:在Linux上用Docker和Jenkins实现Spring Cloud微服务的CI/CD
后端
嘵奇2 小时前
深入解析 Spring Boot 测试核心注解
java·spring boot·后端
uhakadotcom2 小时前
BPF编程入门:使用Rust监控CPU占用
后端·面试·github
uhakadotcom3 小时前
GHSL-2024-252: Cloudflare Workers SDK 环境变量注入漏洞解析
后端·面试·github
uhakadotcom3 小时前
GHSL-2024-264_GHSL-2024-265: 了解 AWS CLI 中的正则表达式拒绝服务漏洞 (ReDoS)
后端·面试·github
Asthenia04123 小时前
Feign的协议和序列化是用的什么?
后端