本篇我们来聊聊 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. [CF431C]k-Tree

    题目描述 Quite recently a creative student Lesha had a lecture on trees. After the lecture Lesha was ins ...

  2. opencv::视频人脸检测

    视频流抓取人脸和眼睛 #include<opencv2/opencv.hpp> #include<iostream> using namespace cv; using nam ...

  3. 利用requests和BeautifulSoup爬取菜鸟教程的代码与图片并保存为markdown格式

    还是设计模式的开卷考试,我想要多准备一点资料,于是写了个爬虫爬取代码与图片,有巧妙地进行格式化进一步处理,最终变为了markdown的格式 import requests from bs4 impor ...

  4. 使用eclipse在tomcat中设置项目启动的虚拟路径

    很多时候我们在启动项目的时候都会在浏览器输入"localhost:+端口号+项目名称" 其实tomcat是可以省去这种麻烦的,通过设置项目的虚拟路径就可访问项目了 第一步 选择ec ...

  5. Java基础(三十四)String、StringBuffer类和数据缓冲区Buffer类

    一.String类 1.创建字符串对象 创建字符串对象有两种方法:直接用“=”或者使用“new String(...)” String aStr = "TMZ"; String b ...

  6. vue cli3.3 以上版本配置vue.config.js

    // vue.config.js 配置说明//官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions// 这里只 ...

  7. Python调试工具

    1. 日志 通过日志或者print来打印变量.必要时可以打印locals()和globals() 建议使用logging.debug()来代替print,这样到了正式环境,就可以统一删除这些日志. 2 ...

  8. SpringBoot之配置文件的注入

    @PropertySource&@ImportResource&@Bean @PropertySource:加载指定的配置文件: /** * 将配置文件中配置的每一个属性的值,映射到这 ...

  9. 【Windows删除指定后缀文件cmd命令】

    如果我想删除指定目录下的"*.mp4"后缀文件 在命令行中,进入指定目录,输入 del [/q] "*.mp4" del 命令是删除文件cmd(命令行)命令. ...

  10. Java自动化测试框架-07 - TestNG之Factory篇 - 欢快畅游梦幻工厂(详细教程)

    简介 最近忙着装修博客园,没时间更新文章,今天终于抽出时间把上次写的一半的文章给写完了,新的博客园风格,希望大家喜欢.今天继续介绍testng的相关知识--工厂. 工厂允许你动态的创建测试.例如,假设 ...