欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

关于subList导致的ConcurrentModificationException异常分析

程序员文章站 2022-05-12 11:33:01
...

由于某老项目中使用的 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 {
			...
        }
        ...
    }

此处留意三四行的clearisNullOrEmpty方法,下面提及;
其中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,并记录了parentmodCount
根据上面BRVAH的check方法,我们就知道我们看的是SubList对象内的size方法,而不是ArrayListsize方法;

    public int size() {
        if (ArrayList.this.modCount != this.modCount)
            throw new ConcurrentModificationException();
        return this.size;
    }

至此我们找到了抛出异常的具体方法;
那么我们来看一下为何SubListmodCount会与其parentmodCount不一致。重新看一下BRVAH的setList方法,我们发现在检查是否为空之前,调用了其内部成员变量dataclear方法,此处的clear即为我们SubListparent由于我们的业务代码中,用来切割的原集合就是从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,相当于我们对同一个对象先进行了subListclear,就会导致同步操作集合的异常。

简化一下整体崩溃代码流程,即:

    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();
    }