关于subList导致的ConcurrentModificationException异常分析
由于某老项目中使用的 BRVAH 为2x版本,所以迁移AndroidX后需要将其一起迁移至3x版本。迁移完毕后运行,发现某页面抛出
ConcurrentModificationException
,在此分析记录一下。
1、异常信息
java.util.ConcurrentModificationException
at java.util.ArrayList$SubList.size(ArrayList.java:1057)
at java.util.AbstractCollection.isEmpty(AbstractCollection.java:86)
at com.chad.library.adapter.base.BaseQuickAdapter.setList(BaseQuickAdapter.kt:1186)
at ......
此异常归根结底是由于在ArrayList
的基类AbstractList
中存在一个变量值modCount
,顾名思义是用来记录修改次数的。当集合存在并发修改时可能会导致该值异常,从而抛出,本文主要分析由于subList
导致该异常抛出的情况。
2、业务代码分析
先来看一下业务代码内容(示例代码),大致含义为从adapter
中取出内容,判断如果不为空,则截取最后一个item
重新赋值。
private void test() {
List<String> data = mAdapter.getData();
if (!data.isEmpty()) {
List<String> subList = data.subList(data.size() - 1, data.size());
mAdapter.setList(subList);
}
}
再来看一下3x版本的BRVAH中BaseQuickAdapter#setList
的方法;
open fun setList(list: Collection<T>?) {
if (list !== this.data) {
this.data.clear()
if (!list.isNullOrEmpty()) {
this.data.addAll(list)
}
} else {
...
}
...
}
此处留意三四行的clear
与isNullOrEmpty
方法,下面提及;
其中log显示异常抛出是在第四行的isNullOrEmpty
方法中,最终会调用集合的size
方法;此处注意一下,我们在业务代码中最终setList
的是subList
之后的一个对象,我们先看一下ArrayList#subList
方法和其返回的对象
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size;
SubList(AbstractList<E> parent, int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
其返回了一个SubList
对象,该类直接继承AbstractList
类,也是Collection
的子类。在构造方法内,其还存储了ArrayList
作为自己的parent
,并记录了parent
的modCount
。
根据上面BRVAH的check
方法,我们就知道我们看的是SubList
对象内的size
方法,而不是ArrayList
的size
方法;
public int size() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
return this.size;
}
至此我们找到了抛出异常的具体方法;
那么我们来看一下为何SubList
的modCount
会与其parent
的modCount
不一致。重新看一下BRVAH的setList
方法,我们发现在检查是否为空之前,调用了其内部成员变量data
的clear
方法,此处的clear
即为我们SubList
的parent
(由于我们的业务代码中,用来切割的原集合就是从adapter中取出的),我们来看一下ArrayList#clear
方法;
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
至此,我们就知道了为何会导致该异常抛出了。
3、扩展思考
崩溃发生后,我在项目中查找运用了subList
方法的地方,发现了另一个场景,同样是使用了subList
之后调用setList
,却没有崩溃,继续使用示例代码分析一下;
导致崩溃
private void test() {
initAdapter();
mAdapter.addData(mLocalArr);
List<String> data = mAdapter.getData();
if (!data.isEmpty()) {
List<String> subList = data.subList(data.size() - 1, data.size());
mAdapter.setList(subList);
}
}
正常运行
private void test() {
initAdapter();
mAdapter.addData(mLocalArr);
List<String> data = mLocalArr;
if (!data.isEmpty()) {
List<String> subList = data.subList(data.size() - 1, data.size());
mAdapter.setList(subList);
}
}
总结一下,即3x版本中的setList
方法中,不会使用你传入的对象存储为mData
,而是将传入的内容取出addAll
。第一种写法中,我们从adapter
中取出集合再进行操作,最终再setList
,相当于我们对同一个对象先进行了subList
再clear
,就会导致同步操作集合的异常。
简化一下整体崩溃代码流程,即:
private void mackCrash(){
List<String> a = new ArrayList<>();
a.add("1");
a.add("2");
//先对a集合进行切割
List<String> b = a.subList(0, 1);
//而后对a集合进行clear
a.clear();
//最终校验对b集合进行校验
boolean empty = b.isEmpty();
}
上一篇: 【java】深度解析单例模式
推荐阅读
-
关于jsp页面使用jstl的异常分析
-
关于RecyclerView嵌套导致item复用异常,界面异常的问题
-
关于扩展 Laravel 默认 Session 中间件导致的 Session 写入失效问题分析
-
MongoDB 执行mongoexport时异常及分析(关于数字类型的查询)
-
关于jsp页面使用jstl的异常分析
-
关于java List的remove方法导致的异常java.util.ConcurrentModificationException
-
关于ip_conntrack跟踪连接满导致网络丢包问题的分析
-
MySQL Bug导致异常宕机的分析流程
-
关于扩展 Laravel 默认 Session 中间件导致的 Session 写入失效问题分析
-
关于RecyclerView嵌套导致item复用异常,界面异常的问题