Java,你告诉我 fail-fast 是什么鬼?
本篇我们来聊聊 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 是什么鬼?的更多相关文章
- Fail Fast and Fail Safe Iterators in Java
https://www.geeksforgeeks.org/fail-fast-fail-safe-iterators-java/ Fail Fast and Fail Safe Iterators ...
- fail fast和fail safe策略
优先考虑出现异常的场景,当程序出现异常的时候,直接抛出异常,随后程序终止 import java.util.ArrayList; import java.util.Collections; impor ...
- 快速失败(fail—fast)和 安全失败(fail—safe)
快速失败(fail-fast) 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的结构进行了修改(增加.删除),则会抛出Concurrent Modification Exception. 原理 ...
- [转]一分钟告诉你究竟DevOps是什么鬼?
本文转自:https://www.cnblogs.com/jetzhang/p/6068773.html 一分钟告诉你究竟DevOps是什么鬼? 历史回顾 为了能够更好的理解什么是DevOps,我 ...
- 【问题】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的其他服务 ...
- FastDFS :java.lang.Exception: getStoreStorage fail, errno code: 28
FastDFS 服务正常,突然报错:java.lang.Exception: getStoreStorage fail, errno code: 28 答:错误代码28表示 No space left ...
- Java集合框架中的快速失败(fail—fast)机制
fail-fast机制,即快速失败机制,是java集合框架中的一种错误检测机制.多线程下用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加.删除),则会抛出Concurre ...
- 动力节点Java培训告诉你Java线程的多功能用法
现在的java开发可谓是八仙过海各显神通啊!遥想当下各种编程语言萎靡不振,而我Java开发异军突起,以狂风扫落叶之态,作为Java培训行业的黄埔军校,为了守护Java之未来,特意总结了一些不被人所熟知 ...
- android studio java.io.IOException:setDataSourse fail.
这一次是针对Android开发中的一个小问题,权限获取的问题. 在写了一个一个小Android程序的时候,有时候普需要获取本机的文件(Audio&Video),这时候如果不加权限就会出现这种情 ...
- 一分钟告诉你究竟DevOps是什么鬼?
历史回顾 为了能够更好的理解什么是DevOps,我们很有必要对当时还只有程序员(此前还没有派生出开发者,前台工程师,后台工程师之类)这个称号存在的历史进行一下回顾. 如编程之道中所言: 老一辈的程序员 ...
随机推荐
- Mac系统 安装Photoshop CC 2018破解版
应用场景 本人从事前端行业,但是工作中有时也需要会点PS技能,之前一直使用window系统,突然换了Mac其他软件基本都差不多安装完了,就剩下比较难搞的PS.刚开始按照网上乱七八槽的教程下载过好多次都 ...
- unittest-A接口的返回结果作为B接口的入参(设置全局变量)
在A接口用例中设置全局变量: globals()["a"] = "用例A的返回结果" 在B接口用例中使用全局变量: b = globals()["a& ...
- 音视频入门-11-PNG文件格式详解
* 音视频入门文章目录 * PNG 文件格式解析 PNG 图像格式文件由一个 8 字节的 PNG 文件署名域和 3 个以上的后续数据块(IHDR.IDAT.IEND)组成. PNG 文件包括 8 字节 ...
- Fiddler抓包和工作原理
一.概述 Fiddler是一款免费且功能强大的数据包抓取软件.它通过代理的方式获取程序http通讯的数据, 可以用其检测网页和服务器的交互情况,能够记录所有客户端和服务器间的http请求, 支持监视. ...
- spring boot 日志收集整理
spring boot 日志 1. 选择那种日志框架 slf4j 是抽像的接口层.也是spring boot 默认采用的接口层. util.logging,log4j,logback,commons- ...
- 自己动手破解Z.EntityFramework.Extensions 4.0.11.0的方法
因为项目中使用到Z.EntityFramework.Extensions 和 Z.EntityFramework.Plus(免费开源)两个类库,但是Z.EntityFramework.Extensio ...
- 基于Groovy搭建Ngrinder脚本调试环境
介绍 最近公司搭建了一套压力测试平台,引用的是开源的项目 Ngrinder,做了二次开发,在脚本管理方面,去掉官方的SVN,引用的是Git,其他就是做了熔断处理等. 对技术一向充满热情的我,必须先来拥 ...
- 解决错误 系统找不到指定的批标签 make_command_arguments |hadoop windows出错
问题:cmd命令行传参数出错 此文章 适用于 cmd命令行传参数出错 在windows 7下倒腾 Hadoop 时出现 The system cannot find the batch label s ...
- 【阿里云IoT+YF3300】8.物联网设备用户脚本开发
除了我们必须熟悉的网页脚本,比如JavaScript.其实在工业自动化中,组态软件是必备脚本的,只是有的脚本语言风格类似C或类似Basic而已.比如昆仑通泰的组态屏中的组态软件.通过安装组态软件可以简 ...
- 条款03:尽肯使用const
定义常量 define 是一个Compile-Time的概念,它的生命周期止于编译器期,它存在与程序的代码段,在实际程序中它只是一个常数.一个命令中的参数.并没有实际的存在 const常量存在于程序的 ...