美文网首页
由一个错误写法想到的 ConcurrentModificatio

由一个错误写法想到的 ConcurrentModificatio

作者: 你可记得叫安可 | 来源:发表于2020-08-08 19:12 被阅读0次

引子

// 下面的代码在一个 Layout 中
private SparseArray<View> mViewByIndex = new SparseArray<>();

private void removeView() {
    for (int i = 0; i < mViewByIndex.size(); i++) {
        int index = mViewByIndex.keyAt(i);
        removeTargetView(index);
    }
}

private void removeTargetView(int index) {
    View targetView = mViewByIndex.get(index);
    removeView(targetView);
    mViewByIndex.remove(index);
}

如上所示逻辑非常简单:一个 mViewByIndex 来保存一个 indexviewMap,遍历 mViewByIndex 取出其中的 targetView,将 targetView 从父 Layout 中移除,同时从 mViewByIndex 中移除该条目。

存在的问题

运行代码我们就会发现并不能清除 mViewByIndex。因为我们在遍历 mViewByIndex 的同时也在修改 mViewByIndex 的结构。这个显然是不正确的,但是为什么没有抛出我所熟悉的 ConcurrentModificationException 呢?而且也没有 OutOfIndexException 这样的异常,而是正确运行完了。为什么没有发生异常,我们就需要看看 Java 实现中,ConcurrentModificationException 异常是怎么抛出的?

ConcurrentModificationException 初印象及其抛出原理

开始时,我以为 ConcurrentModificationException 只有在多线程访问 List 时会出现,因为有个 Concurrent 单词表示并发。但其实并不然,在单线程的时候依然会出现。但是为什么我们上面的例子却没有出现呢?我们在源码中搜索 ConcurrentModificationException,可以看到抛出这个异常的地方几乎都是在迭代器的实现中。迭代器中抛出异常的原理是:在迭代器创建的时候,迭代器记录下当前 List 中元素的数量作为初始数量 expectedModCount。当开发者每次调用迭代器去访问元素时,迭代器都比较一下当前元素数量 modCountexpectedModCount 是否相等,如果不相等则会抛出 ConcurrentModificationException

解决引子中的问题

可见 ConcurrentModificationException 异常是由迭代器自己检查的,跟是否用于多线程没有关系。只不过多线程环境下,这个异常更容易被触发。回到上面的例子,由于 SparseArray 没有迭代器,因此我们使用了传统的遍历方式,根据迭代器的原理,每轮循环我们去访问 SparseArray 时就应该自己去检查数组的大小是否已经被改变过,因为如果不检查的话,我们的遍历就会少遍历元素,但是却没有任何的报错。如下所示代码,我们只会遍历到 1,3:

private void test() {
    integerList.add(1);
    integerList.add(2);
    integerList.add(3);
    for (int i = 0; i < integerList.size(); i++) {
        System.out.println("" + integerList.get(i) + " ");
        integerList.remove(i);
    }
}
// 只会遍历 1,3

要避免这种难以察觉的错误发生,我们有三个选择:

  1. 不要每轮循环都是用当前列表大小作为循环的退出条件,而是跟迭代器一样,使用列表的初始值。这样不能保证完成遍历功能,却能在出错时抛出 IndexOutOfBoundsException
private void test() {
    integerList.add(1);
    integerList.add(2);
    integerList.add(3);
    int size = integerList.size();
    for (int i = 0; i < size; i++) {
        System.out.println("" + integerList.get(i) + " ");
        integerList.remove(i);
    }
}
// 能够不造成错误的遍历,而是抛出 IndexOutOfBoundsException 异常
  1. 使用迭代器。在使用的迭代器中进行增删改查时,迭代器会自动更新链表的初始大小 expectedModCount,因此我们使用迭代器去一边遍历一遍修改 ArrayList,而不会出错:
private void test() {
    integerList.add(1);
    integerList.add(2);
    integerList.add(3);

    integerList.forEach(integer -> integerList.remove(integer));
    Iterator<Integer> iterator = integerList.iterator();
    while (iterator.hasNext()) {
        System.out.println("" + iterator.next());
        iterator.remove();
    }
}
// 在单线程环境下,能够正确遍历,而不会报错

但是在多线程环境下是会出现 ConcurrentModificationException 的,这是因为 ArrayList 的迭代器操作并不是线程安全的。

  1. 但是不能使用迭代器的 forEach 方法来像下面一样不通过迭代器直接删除元素。这是因为 forEach 中实际上使用了迭代器,但是在它的 Consumer 里移除元素时却没有通过迭代器来移除。因此会抛出 ConcurrentModificationException 异常。
private void test() {
    integerList.add(1);
    integerList.add(2);
    integerList.add(3);
    integerList.forEach(integer -> integerList.remove(integer));
}
// 抛出 ConcurrentModificationException

相关文章

网友评论

      本文标题:由一个错误写法想到的 ConcurrentModificatio

      本文链接:https://www.haomeiwen.com/subject/egbtdktx.html