fast-fail机制是一种错误检测机制,在遍历元素时很常见。虽然很基础,但是突然被问起来,靠我的三秒记忆还真不容易说清楚,所以来记录下,方便回忆和复习~
文章内出现的源码来自jdk1.8版本
大纲
什么是fast-fail
举个生活中的例子,在上高中的时后,我们每天早上跑操的时候需要统计出勤的人数,我会负责去数今天到了多少人。
报数! 1 2 3 4
/ / / / /
( `д´) (`ヮ´) (`ヮ´) (`ヮ´) (`ヮ´)
但是!有些不想跑操的同学会在统计完人数后,悄悄溜走。导致实际上出勤人数和统计的预期人数不符合。
报告出勤4人 这不是只有三人吗 报完数溜啰
/ / /
( `д´)( ´_ゝ`) (`ヮ´ )σ`∀´) ゚∀゚)σ ᕕ( ᐛ )ᕗ
而fast-fail机制,就是一种针对这种情况的错误机制。将同学们看作集合里的元素,统计人数的我就是迭代器,当迭代器发现集合的结构发生改变时,直接抛出ConcurrentModificationException异常
出现场景
fast-fail在单线程和多线程都会出现。只要迭代器遍历着发现集合结构和之前不同了,都会报错:
public class FastFailDemo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
Iterator iterator = list.iterator(); // 获取迭代器
while (iterator.hasNext()) {
// add和remove修改了原有结构,会导致报错
list.add(7);
list.remove(2);
// set虽然修改了元素,但是结构未修改,不会导致报错
list.set(0, 7);
System.err.println(iterator.next()); // 在这一行报错 java.util.ConcurrentModificationException
}
}
}
在有subList的情况下,如果对原集合元素进行了增加或删除,且又对subList进行了遍历或增删操作,也会抛出异常:
public class FastFailDemo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
Iterator<Integer> iterator = list.iterator();
List<Integer> subList = list.subList(0, 3);
System.out.println(subList); // 正常遍历[1, 2, 3]
subList.add(7);
System.out.println(subList); // 报错 java.util.ConcurrentModificationException
}
}
源码解读
1. 什么是modCount
ArrayList
继承的AbstractList
里有一个关键的成员变量modCount
记录了集合结构被修改的次数。源码中对modCount
描述为:
modCount为表示列表结构修改的次数。结构修改是指集合大小的变化,避免改变使得在迭代中产生不正确的结果。该值在迭代器中使用到,如果该值的变化不符合预期,迭代器会在调用next remove previous set add
方法时抛出异常ConcurrentModificationException
2. 调用ArrayList的方法修改List结构
然后查看会对列表结构产生修改的方法:add
在第一行,就是增加modCount的方法,然后才是真正的把新元素插入集合,转跳到具体的修改部分:
总结下add方法的大致流程是:
- 计算增加元素后的大小(最少需要多大的容量)
- 增加修改次数
modCount++
- 容量若不够需要扩容(原容量的1.5倍,不是这个文章的重点就不细说了)
- 将新元素加入集合
3. 迭代器遍历抛出异常
然后再看报错的迭代器部分:
迭代器由ArrayList
的内部类Itr
实现,在构造迭代器时,将List现有的modCount赋值给迭代器的属性。
在next方法的第一步就是先校验:集合结构是否被修改过
由于创建迭代器后,又对list进行了两次add操作,所以modCount != expectedModCount
返回true
,导致报错。
4. 使用迭代器改变List结构,不抛出异常
迭代器中存在方法Iterator#remove,这个方法用来删除元素不会报错,因为迭代器的删除不会修改modCount,检查时,modCount != expectedModCount
返回false
5. 修改原集合,导致subList遍历报错
子列也是通过内部类,初始属性modCount取现原集合modCount实现的。
在执行list.add之前,subsList能正常遍历。在执行list.add之后,原list的modCount值增加为5,而子类的modCount没有增加,保持为4。两值不等抛出异常。
6. subList修改集合,不抛出异常
在调用ArrayList.SubList#add方法时:
- 先检查是否超出了下标范围
- 再检查了现在modCount是否符合预期
- 调用了ArrayList#add方法,此时原list的modCount增加1
- 然后将新的modCount赋值给subList的modCount,保持了subList和原list的modCount相等