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

相关推荐
苏打水com23 分钟前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
间彧1 小时前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧1 小时前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧1 小时前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧1 小时前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧2 小时前
Spring Cloud Gateway详解与应用实战
后端
EnCi Zheng3 小时前
SpringBoot 配置文件完全指南-从入门到精通
java·spring boot·后端
烙印6013 小时前
Spring容器的心脏:深度解析refresh()方法(上)
java·后端·spring
Lisonseekpan3 小时前
Guava Cache 高性能本地缓存库详解与使用案例
java·spring boot·后端·缓存·guava
4 小时前
JUC专题 - 并发编程带来的安全性挑战之同步锁
后端