本篇我们来聊聊 Java 的 fail-fast 机制,文字一如既往的有趣哦。

01、前言

说起来真特么惭愧:十年 IT 老兵,Java 菜鸟一枚。今天我才了解到 Java 还有 fail-fast 一说。不得不感慨啊,学习真的是没有止境。只要肯学,就会有巨多巨多别人眼中的“旧”知识涌现出来,并且在我这全是新的。

能怎么办呢?除了羞愧,就只能赶紧全身心地投入学习,把这些知识掌握。

为了镇楼,必须搬一段英文来解释一下 fail-fast。

In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system's state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them.

大家不嫌弃的话,我就用蹩脚的英语能力翻译一下。某场战役当中,政委发现司令员在乱指挥的话,就立马报告给权限更高的中央军委——这样可以有效地避免更严重的后果出现。当然了,如果司令员是李云龙的话,报告也没啥用。

不过,Java 的世界里不存在李云龙。fail-fast 扮演的就是政委的角色,一旦报告给上级,后面的行动就别想执行。

怎么和代码关联起来呢?看下面这段代码。

public void test(Wanger wanger) {
if (wanger == null) {
throw new RuntimeException("wanger 不能为空");
} System.out.println(wanger.toString());
}

一旦检测到 wanger 为 null,就立马抛出异常,让调用者来决定这种情况下该怎么处理,下一步 wanger.toString() 就不会执行了——避免更严重的错误出现,这段代码由于太过简单,体现不出来,后面会讲到。

瞧,fail-fast 就是这个鬼,没什么神秘的。如果大家源码看得比较多的话,这种例子多得就像旅游高峰期的人头。

然后呢,没了?三秒钟,别着急,我们继续。

02、for each 中集合的 remove 操作

很长一段时间里,都不明白为什么不能在 for each 循环里进行元素的 remove。今天我们就来借机来体验一把。

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员"); for (String str : list) {
if ("沉默王二".equals(str)) {
list.remove(str);
}
} System.out.println(list);

这段代码看起来没有任何问题,但运行起来就糟糕了。

Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.cmower.java_demo.str.Cmower3.main(Cmower3.java:14)

为毛呢?

03、分析问题的杀手锏

这时候就只能看源码了,ArrayList.java 的 909 行代码是这样的。

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

也就是说,remove 的时候执行了 checkForComodification 方法,该方法对 modCount 和 expectedModCount 进行了比较,发现两者不等,就抛出了 ConcurrentModificationException 异常。

可为什么会执行 checkForComodification 方法呢?这就需要反编译一下 for each 那段代码了。

List<String> list = new ArrayList();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");
Iterator var3 = list.iterator(); while (var3.hasNext()) {
String str = (String) var3.next();
if ("沉默王二".equals(str)) {
list.remove(str);
}
} System.out.println(list);

原来 for each 是通过迭代器 Iterator 配合 while 循环实现的。

1)ArrayList.iterator() 返回的 Iterator 其实是 ArrayList 的一个内部类 Itr。

public Iterator<E> iterator() {
return new Itr();
}

Itr 实现了 Iterator 接口。

private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount; Itr() {} public boolean hasNext() {
return cursor != size;
} @SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
}

也就是说 new Itr() 的时候 expectedModCount 被赋值为 modCount,而 modCount 是 List 的一个成员变量,表示集合被修改的次数。由于 list 此前执行了 3 次 add 方法,所以 modCount 的值为 3;expectedModCount 的值也为 3。

可当执行 list.remove(str) 后,modCount 的值变成了 4。

private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}

注:remove 方法内部调用了 fastRemove 方法。

下一次循环执行到 String str = (String) var3.next(); 的时候,就会调用 checkForComodification 方法,此时一个为 3,一个为 4,就只好抛出异常 ConcurrentModificationException 了。

不信,可以直接在 ArrayList 类的 909 行打个断点 debug 一下。

真的耶,一个是 4 一个是 3。

总结一下。在 for each 循环中,集合遍历其实是通过迭代器 Iterator 配合 while 循环实现的,但是元素的 remove 却直接使用的集合类自身的方法。这就导致 Iterator 在遍历的时候,会发现元素在自己不知情的情况下被修改了,它觉得很难接受,就抛出了异常。

读者朋友们,你们是不是觉得我跑题了,fail-fast 和 for each 中集合的 remove 操作有什么关系呢?

有!Iterator 使用了 fail-fast 的保护机制。

04、怎么避开 fail-fast 保护机制呢

通过上面的分析,相信大家都明白为什么不能在 for each 循环里进行元素的 remove 了。

那怎么避开 fail-fast 保护机制呢?毕竟删除元素是常规操作,咱不能因噎废食啊。

1)remove 后 break

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员"); for (String str : list) {
if ("沉默王二".equals(str)) {
list.remove(str);
break;
}
}

我怎么这么聪明,忍不住骄傲一下。有读者不明白为什么吗?那我上面的源码分析可就白分析了,爬楼再看一遍吧!

略微透露一下原因:break 后循环就不再遍历了,意味着 Iterator 的 next 方法不再执行了,也就意味着 checkForComodification 方法不再执行了,所以异常也就不会抛出了。

但是呢,当 List 中有重复元素要删除的时候,break 就不合适了。

2)for 循环

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");
for (int i = 0, n = list.size(); i < n; i++) {
String str = list.get(i);
if ("沉默王二".equals(str)) {
list.remove(str);
}
}

for 循环虽然可以避开 fail-fast 保护机制,也就说 remove 元素后不再抛出异常;但是呢,这段程序在原则上是有问题的。为什么呢?

第一次循环的时候,i 为 0,list.size() 为 3,当执行完 remove 方法后,i 为 1,list.size() 却变成了 2,因为 list 的大小在 remove 后发生了变化,也就意味着“沉默王三”这个元素被跳过了。能明白吗?

remove 之前 list.get(1) 为“沉默王三”;但 remove 之后 list.get(1) 变成了“一个文章真特么有趣的程序员”,而 list.get(0) 变成了“沉默王三”。

3)Iterator

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员"); Iterator<String> itr = list.iterator(); while (itr.hasNext()) {
String str = itr.next();
if ("沉默王二".equals(str)) {
itr.remove();
}
}

为什么使用 Iterator 的 remove 方法就可以避开 fail-fast 保护机制呢?看一下 remove 的源码就明白了。

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification(); try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

虽然删除元素依然使用的是 ArrayList 的 remove 方法,但是删除完会执行 expectedModCount = modCount,保证了 expectedModCount 与 modCount 的同步。

05、最后

在 Java 中,fail-fast 从狭义上讲是针对多线程情况下的集合迭代器而言的。这一点可以从 ConcurrentModificationException 定义上看得出来。

This exception may be thrown by methods that have detected concurrent

modification of an object when such modification is not permissible.

For example, it is not generally permissible for one thread to modify a Collectionwhile another thread is iterating over it. In general, the results of theiteration are undefined under these circumstances. Some Iteratorimplementations (including those of all the general purpose collection implementationsprovided by the JRE) may choose to throw this exception if this behavior isdetected. Iterators that do this are known as fail-fast iterators,as they fail quickly and cleanly, rather that risking arbitrary,non-deterministic behavior at an undetermined time in the future.

再次拙劣地翻译一下。

该异常可能由于检测到对象在并发情况下被修改而抛出的,而这种修改是不允许的。

通常,这种操作是不允许的,比如说一个线程在修改集合,而另一个线程在迭代它。这种情况下,迭代的结果是不确定的。如果检测到这种行为,一些 Iterator(比如说 ArrayList 的内部类 Itr)就会选择抛出该异常。这样的迭代器被称为 fail-fast 迭代器,因为尽早的失败比未来出现不确定的风险更好。

既然是针对多线程,为什么我们之前的分析都是基于单线程的呢?因为从广义上讲,fail-fast 指的是当有异常或者错误发生时就立即中断执行的这种设计,从单线程的角度去分析,大家更容易明白。

你说对吗?

06、致谢

谢谢大家的阅读,原创不易,喜欢就随手点个赞

Java,你告诉我 fail-fast 是什么鬼?的更多相关文章

  1. Fail Fast and Fail Safe Iterators in Java

    https://www.geeksforgeeks.org/fail-fast-fail-safe-iterators-java/ Fail Fast and Fail Safe Iterators ...

  2. fail fast和fail safe策略

    优先考虑出现异常的场景,当程序出现异常的时候,直接抛出异常,随后程序终止 import java.util.ArrayList; import java.util.Collections; impor ...

  3. 快速失败(fail—fast)和 安全失败(fail—safe)

    快速失败(fail-fast) 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的结构进行了修改(增加.删除),则会抛出Concurrent Modification Exception. 原理 ...

  4. [转]一分钟告诉你究竟DevOps是什么鬼?

    本文转自:https://www.cnblogs.com/jetzhang/p/6068773.html 一分钟告诉你究竟DevOps是什么鬼?   历史回顾 为了能够更好的理解什么是DevOps,我 ...

  5. 【问题】Could not locate PropertySource and the fail fast property is set, failing

    这是我遇到的问题 Could not locate PropertySource and the fail fast property is set, failing springcloud的其他服务 ...

  6. FastDFS :java.lang.Exception: getStoreStorage fail, errno code: 28

    FastDFS 服务正常,突然报错:java.lang.Exception: getStoreStorage fail, errno code: 28 答:错误代码28表示 No space left ...

  7. Java集合框架中的快速失败(fail—fast)机制

      fail-fast机制,即快速失败机制,是java集合框架中的一种错误检测机制.多线程下用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加.删除),则会抛出Concurre ...

  8. 动力节点Java培训告诉你Java线程的多功能用法

    现在的java开发可谓是八仙过海各显神通啊!遥想当下各种编程语言萎靡不振,而我Java开发异军突起,以狂风扫落叶之态,作为Java培训行业的黄埔军校,为了守护Java之未来,特意总结了一些不被人所熟知 ...

  9. android studio java.io.IOException:setDataSourse fail.

    这一次是针对Android开发中的一个小问题,权限获取的问题. 在写了一个一个小Android程序的时候,有时候普需要获取本机的文件(Audio&Video),这时候如果不加权限就会出现这种情 ...

  10. 一分钟告诉你究竟DevOps是什么鬼?

    历史回顾 为了能够更好的理解什么是DevOps,我们很有必要对当时还只有程序员(此前还没有派生出开发者,前台工程师,后台工程师之类)这个称号存在的历史进行一下回顾. 如编程之道中所言: 老一辈的程序员 ...

随机推荐

  1. Luogu1119灾后重建

    题目背景 BBB 地区在地震过后,所有村庄都造成了一定的损毁,而这场地震却没对公路造成什么影响.但是在村庄重建好之前,所有与未重建完成的村庄的公路均无法通车.换句话说,只有连接着两个重建完成的村庄的公 ...

  2. JBOSS中间件漏洞总汇复现

    JBOSS中间件漏洞总汇复现 JBoss JMXInvokerServlet 反序列化漏洞 漏洞复现 直接使用docker搭建的漏洞环境. 环境搭建完成后,直接使用工具检测即可:工具下载地址https ...

  3. HDU 6112 今夕何夕 (预处理 枚举)

    中文题意都看的懂啦~ 思路很简单,就是通过前一天推出当天是星期几,直接枚举所有2017-9999年的每一天就好了.ㄟ( ▔, ▔ )ㄏ 代码: #include <cstdio> #def ...

  4. golang会取代php吗

    看看PHP和Golang如何在开发速度,性能,安全性,可伸缩性等方面展开合作. PHP与Golang比较是一个艰难的比较. PHP最初创建于1994年,已有24年.自那时起,由于PHP的开源格式,易用 ...

  5. 星云测试插装编译流程与CI集成

    星云测试Horn插装采用脚本配置方式自动对语法进行扫描和插装,在整个插装过程中需要用到星云提供的插件工具.通过与CI集成,在CI编译前通过jenkins调用星云插装插件模块进行必要的数据填充,生成对应 ...

  6. 钢铁B2B电商案例:供应链金融如何解决供应链金融痛点

    一.区块链是什么 区块链是一种按照时间顺序将数据块以特定的顺序相连的方式组合成的链式数据结构,其上存储了系统诞生以来所有交易的记录.区块链上的数据由全网节点共同维护并共同存储,同时以密码学方式保证区块 ...

  7. Veins(车载通信仿真框架)入门教程(四)——调试及记录结果

    Veins(车载通信仿真框架)入门教程(四)——调试及记录结果 在Veins入门教程(三)最后的动图中(如下图)可以看到大大小小的光圈,这个怎么实现的呢? 很简单,以收到RTS消息为例,通过finHo ...

  8. jquery引用

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  9. java中的Static、final、Static final各种用法详解

    前言 对Static.final.Static final这几个关键词熟悉又陌生?想说却又不知怎么准确说出口?好的,本篇博客文章将简短概要出他们之间的各自的使用,希望各位要是被你的面试官问到了,也能从 ...

  10. js自增图片切换

    使用js自增进行图片的切换 <!DOCTYPE html> <html lang="zh"> <head> <meta charset=& ...