前两天小伙伴问我关于集合快速失败的问题,勾起了我对快速失败机制 fail-fast 的回忆,这两天也仔细看了一下相关内容,在此做一下总结。
先放出小伙伴提的问题,以及对应的测试代码,我稍后会细讲这个话题——为什么没有快速迭代失败?
fail-fast
先来讲一下 fail-fast,它实际上是一种编程思想,即快速反馈系统错误,防止发生更严重的问题。我们在平时的开发中肯定写过类似的代码,提前判空也属于一种快速失败机制的实现,防止后续出现未处理的空指针异常。
public void submitForm(Info info) {
if (info == null) {
// 异常处理逻辑
}
// 后续处理逻辑
}
Arraylist快速失败
先来看一段代码,在遍历 list 的过程中,移除 list 元素。
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
for (String s : list) {
System.out.println(s);
list.remove(s);
}
System.out.println(list.size());
}
运行结果如下:
这边抛出了一个 ConcurrentModificationException 异常,是什么原因导致的异常呢?下面就要提到 Arraylist 的快速失败机制了。
实现原理
将上述测试代码反编译,发现底层使用的是迭代器进行集合遍历。
public static void main(String[] args) {
ArrayList<String> list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String s = (String)var2.next();
System.out.println(s);
list.remove(s);
}
System.out.println(list.size());
}
再看到迭代器遍历元素使用的 next() 方法
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
}
在遍历前需要检查,看到 checkForComodification() 方法,这里可能会抛出一个 ConcurrentModificationException 异常,依据的是 modCount != expectedModCount 条件
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
这边简单解释一下两个变量:
- modCount 记录集合的修改次数,任何涉及元素变化的操作都会修改这个值,例如 add, remove
- expectedModCount,迭代器创建时会用 expectedModCount 记录那个时刻的的修改次数快照,即 expectedModCount = modCount
expectedModCount 的值是静态的,而 modCount 则是动态变化的,这就很好理解上面代码为什么会抛出 ConcurrentModificationException 异常,在遍历集合的过程中,同时修改了集合元素导致 modCount 发生变化,使得 modCount != expectedModCount。
为什么没有快速迭代失败?
既然明白了快速迭代失败的实现机制,再回到开头小伙伴提到的问题,为什么没有快速迭代失败?看到测试代码。
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
for (String s : list) {
if ("2".equals(s)) {
list.remove(s);
}
}
System.out.println(list.size());
}
运行结果:
这边有个很有意思的现象,包括我之前也没有注意到,**如果在遍历集合的过程中,移除倒数第 2 个元素,会发现没有 fail-fast 的异常抛出,**即小伙伴提出的问题。
这就涉及迭代器迭代过程中,边界值的检验问题了。
while(var2.hasNext()) {
String s = (String)var2.next();
if ("2".equals(s)) {
list.remove(s);
}
}
看到 hasNext() 方法代码,它决定是否要继续遍历集合,简单说明 cursor 用来表示下一个即将要遍历到的元素索引,而 size 则是集合当前的元素数量。
public boolean hasNext() {
return cursor != size;
}
这就很好理解了,迭代过程如下:
-
假设遍历到了倒数第二个元素,此时 cursor = size - 1,cursor 从 0 开始计数;
-
之后进行 list.remove(s),造成 size - 1;
-
再回到 hasNext() 进行判断,此时 cursor == size 刚好不满足继续遍历的条件,直接退出迭代,也就没有了后续 checkForComodification() 检查
设计思想
上述例子,通过单线程模拟了 ArrayList 存在的线程安全问题,即多线程并发的情况下,可能存在 A 线程正在遍历集合,B 线程同时修改了集合元素的问题,另外通过 fail-fast 机制将安全隐患通知到开发者,下面来看一下 fail-fast 机制的设计思想。
这边引用 jdk ArrayList 源码中的一段话。
Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis.
Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs
我们知道 ArrayList 是非同步容器,fail-fast 是为可能发生的并发问题提供一种预警机制,这也是ConcurrentModificationException 异常名称由来,另外既然是预警机制,它并不能保证一定抛出这个异常,只能说尽力去抛出,所以作者也说明了——fail fast 应当用来监控 bug,而不是企图依赖此异常进行其它的程序处理。
在《Java 并发编程实战》一书中就提到,此书的作者包括 ArrayList 源码的开发者 Josh Bloch。
这是一种设计上的平衡,从而降低并发修改操作的检测代码对程序性能带来的影响。
同传统的同步容器 Vector,Hashtable 等不同,ArrayList 实现并没有涉及加锁操作,在保留高性能读写特性的同时,也通过 fail-fast 机制向开发者发出线程安全问题的预警,如此一来,ArrayList 容器的自由度也更高了,让用户自己决定需要不要额外加锁。
总结
通过本文,我们主要了解了:
- 什么是 fail-fast 机制?
- ArrayList 的快速失败实现
- 为什么没有快速迭代失败?
- jdk 容器中 fail-fast 机制的设计思想
在下一篇文章中,我会继续提到另外一种机制——fail-safe,即安全失败机制。如果觉得文章对你有帮助,欢迎留言点赞。