在初始化三方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
...
修正
- 最直观的是为
addElement
方法加把锁🔐,保证多线程执行时 是一个一个来的,保证可见性和一致性
-
或者换成线程安全的集合,如:
Collections.synchronizedList
ConcurrentLinkedQueue
arduino
private final List<String> listeners = Collections.synchronizedList(new ArrayList<>());
private final ConcurrentLinkedQueue<String> listeners = new ConcurrentLinkedQueue<>();
该场景下,不推荐使用 CopyOnWriteArrayList
,因为它会在每次写入操作时创建一个新的副本,这可能会导致性能问题和不断增长的内存占用,在多线程情况下可能会导致死锁。