fail-fast简介
在java.util包下的集合类中,有一个成员变量modCount。
protected transient int modCount = 0;
这个变量的数值表示使集合发生结构变化的次数。
引起结构变化的操作指诸如影响集合大小(新增、删除元素)或干扰遍历结果的操作。
原理简述
当调用会改变集合结构的操作,如添加、删除元素时,会使变量modCount的数值增加1,以此来记录当前集合的结构被修改了多少次。迭代器被实例话的时候,其内部也会拷贝一份该数值,存入迭代器成员变量expectedModCount中。在迭代器操作集合(如netx()、remove() )之前会校验expectedModCount与集合中的modCount值是否一致,若不一致,则说明在遍历过程中,集合的结构发生了变化,此时迭代器会立即终止对集合的操作,抛出ConcurrentModificationException异常。
实现机制解析
我们以ArrayList为例,从源码入手看看这个机制是如何工作的。
- 实例化一个新的ArrayList,并为它添加5个元素。
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 5; i ++) {
list.add(i);
}
在add()方法中我们可以看到modCount累加的操作。
private void ensureExplicitCapacity(int minCapacity) {
modCount++; //操作数自增一
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
由于我们使用循环依次添加了5个元素,也就是调用了5次add方法,因此结构应该发生了5次变化,此时modCount的数值应该为5。
modCount
- 尝试触发fail-fast
此时我们已经有了一个包含了5个元素的ArrayList,现在可以尝试触发fail-fast了。
模拟一个需求:遍历该List,如果当前元素是偶数,则从删除集合内删除。
很多初学者会想到如下实现:
for (Integer num : list) {
if (num % 2 == 0) {
list.remove(num);
}
}
事实证明,该代码在执行时会抛出异常,而该异常正是ConcurrentModificationException异常。
- 如何触发的fail-fast呢?
for-each(又称增强for)与普通的for循环遍历不同,它实际上调用的是集合的迭代器,它的效率比普通for循环要高。
我们来看看上面这段代码发生了什么。
迭代器如图可见,for-each调用了ArrayList的迭代器,此时迭代器的expectedModCount已经被modCount赋值为5。
当调用next()方法获取元素时,首选会调用checkForComodification()方法检查集合结构是否被修改。
checkForComodification
而检查的依据就是判断expectedModCount和modCount的值是否相等。
checkForComodification内部
由于现在是遍历操作,不会引起集合结构变化,所以modCount值没有改变,第一次遍历检查通过,成功拿到第一个元素0,并且由于0是偶数,因此它被成功删除。
0已经不见了
继续执行遍历,由于刚才执行了remove()方法,list的结构发生变化,modCount也随之自增,此时expectedCount已经和modCount不同了。
modCount发生改变
这时迭代器的checkForComodification方法检测到两个数值不一致,抛出了ConcurrentModificationException异常。
ConcurrentModificationException
- 如何避免ConcurrentModificationException异常呢
针对上面模拟的需求,我们应该使用迭代器遍历删除。
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
if (iterator.next() % 2 == 0) {
iterator.remove();
}
}
迭代器remove
此时没有触发ConcurrentModificationException异常是因为迭代器的remove()方法中,在删除元素后,将expectedModCount的值做了同步。
但在并发环境下解决这个问题还应该使用java.util.current包下的集合类。该包下的集合类在修改集合结构时没有在原集合直接修改,而是将数据复制到一个新数组内,在新数组内修改后再将值同步到原数组中。
下面是java.util.current.CopyOnWriteArrayList类的说明
* A thread-safe variant of {@link java.util.ArrayList} in which all mutative
* operations ({@code add}, {@code set}, and so on) are implemented by
* making a fresh copy of the underlying array.
网友评论