引子
// 下面的代码在一个 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
来保存一个 index
到 view
的 Map
,遍历 mViewByIndex
取出其中的 targetView
,将 targetView
从父 Layout
中移除,同时从 mViewByIndex
中移除该条目。
存在的问题
运行代码我们就会发现并不能清除 mViewByIndex
。因为我们在遍历 mViewByIndex
的同时也在修改 mViewByIndex
的结构。这个显然是不正确的,但是为什么没有抛出我所熟悉的 ConcurrentModificationException
呢?而且也没有 OutOfIndexException
这样的异常,而是正确运行完了。为什么没有发生异常,我们就需要看看 Java 实现中,ConcurrentModificationException
异常是怎么抛出的?
ConcurrentModificationException 初印象及其抛出原理
开始时,我以为 ConcurrentModificationException
只有在多线程访问 List
时会出现,因为有个 Concurrent 单词表示并发。但其实并不然,在单线程的时候依然会出现。但是为什么我们上面的例子却没有出现呢?我们在源码中搜索 ConcurrentModificationException
,可以看到抛出这个异常的地方几乎都是在迭代器的实现中。迭代器中抛出异常的原理是:在迭代器创建的时候,迭代器记录下当前 List
中元素的数量作为初始数量 expectedModCount
。当开发者每次调用迭代器去访问元素时,迭代器都比较一下当前元素数量 modCount
和 expectedModCount
是否相等,如果不相等则会抛出 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
要避免这种难以察觉的错误发生,我们有三个选择:
- 不要每轮循环都是用当前列表大小作为循环的退出条件,而是跟迭代器一样,使用列表的初始值。这样不能保证完成遍历功能,却能在出错时抛出
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 异常
- 使用迭代器。在使用的迭代器中进行增删改查时,迭代器会自动更新链表的初始大小
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
的迭代器操作并不是线程安全的。
- 但是不能使用迭代器的
forEach
方法来像下面一样不通过迭代器直接删除元素。这是因为forEach
中实际上使用了迭代器,但是在它的Consumer
里移除元素时却没有通过迭代器来移除。因此会抛出ConcurrentModificationException
异常。
private void test() {
integerList.add(1);
integerList.add(2);
integerList.add(3);
integerList.forEach(integer -> integerList.remove(integer));
}
// 抛出 ConcurrentModificationException
网友评论