java.util.ArrayList的length与size为何不一致触发索引-1越界?
异常原因说明
你遇到的异常核心是多线程并发操作非线程安全的ArrayList导致的状态不一致,具体拆解如下:
- 异常日志里的两个参数含义完全独立:
- 异常中打印的
length=22是ArrayList底层存储元素的缓存数组的容量,不代表当前列表实际存储的元素数量,只代表当前底层数组最多可以无需扩容就存储22个元素。 - 索引为
-1是调用myList.get()时传入的参数,说明调用myList.size()获取到的列表实际元素数量是0。
- 异常中打印的
- 完整的异常触发时序如下:
ArrayList本身没有加任何同步锁,支持并发读写但会出现状态异常。你的场景中myList被多个线程同时操作:一个线程负责执行get(myList.size() -1)逻辑(也就是Retrofit回调所在的线程),其他线程同时在修改myList的元素。- 第一步:回调线程先执行
myList.size(),此时刚好其他线程刚完成myList.clear()操作,size变量被更新为0,因此计算得到索引值为-1。 - 第二步:在回调线程拿着计算好的
-1执行get()方法前的间隙,其他线程往myList中添加了多个元素,触发底层缓存数组扩容到长度22,同时把size变量更新为大于0的值。 - 第三步:回调线程执行
get(-1),由于索引是负数,ArrayList的范围校验逻辑index >= size(此时size已经是大于0的值)不触发,直接访问底层数组的-1下标,JVM抛出ArrayIndexOutOfBoundsException,异常信息里打印的是当前底层数组的容量22和你传入的索引-1,就出现了你看到的矛盾日志。
修复方案
- 优先替换非线程安全的
ArrayList为线程安全的CopyOnWriteArrayList,从根源避免并发修改的状态异常。 - 如果要保留
ArrayList,可以对所有读写myList的逻辑加统一的同步锁,或者在取尾元素时先把size存到局部变量再做判断,避免两次读取size的时间差内被修改:
int currentSize = myList.size(); if (currentSize > 0) { // 直接用已读取的currentSize计算索引,避免二次读取size Object lastItem = myList.get(currentSize - 1); }
内容的提问来源于stack exchange,提问作者Adam Johns
相关产品推荐
相关产品推荐

