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,我们很有必要对当时还只有程序员(此前还没有派生出开发者,前台工程师,后台工程师之类)这个称号存在的历史进行一下回顾. 如编程之道中所言: 老一辈的程序员 ...
随机推荐
- python学习-语言概述(一)
1.python的特点 python是一种面向对象.解释型.弱类型的脚本语言,它也是一种功能强大而完善的通用型语言. 解释性语言的特点:速度慢:源代码加密困难:跨平台:
- 4. NFS存储服务器搭建
1.什么是NFS? Network file system 网络文件系统 nfs共享存储 2.nfs能干什么? nfs 能为 不同主机系统之间 实现 文件的共享 3.为什么要使用nfs? 在集群架构中 ...
- 《编写可维护的JavaScript》 笔记
<编写可维护的JavaScript> 笔记 我的github iSAM2016 概述 本书的一开始介绍了大量的编码规范,并且给出了最佳和错误的范例,大部分在网上的编码规范看过,就不在赘述 ...
- Unity Dropdown
unity DropDown控件应用很简单 代码如下 frameDpdown.options.Clear(); //Dropdown.OptionData optDataFrame = new Dro ...
- spring cloud 2.x版本 Eureka Client服务提供者教程
本文采用Spring cloud本文为2.1.8RELEASE,version=Greenwich.SR3 1 创建eureka client 1.1 新建Srping boot工程:eureka-c ...
- Logstash 安装及简单实用(同步MySql数据到Elasticsearch)(Windows)
Logstash是一款轻量级的日志搜集处理框架,可以方便的把分散的.多样化的日志搜集起来,并进行自定义的处理,然后传输到指定的位置,比如某个服务器或者文件 Windows环境: 1.下载logstas ...
- zabbix导入数据库报错1046 (3D000) : No database selected
Zabbix导入数据库时报错 使用如下命令导入Zabbix数据库时报错 zcat /usr/share/doc/zabbix-server-mysql/create.sql.gz | mysql -u ...
- 以Mnist为例从头开始自己建立数据集,搭建resnet34,识别Mnist
写在前面: 本人小白研一,刚开始学习深度学习,将自己的第一个实验过程总结下来,看了很多的大牛的博客,在下面的程序中也参考了很多大牛的博客.在刚开始入门的学习的时候,直接编写程序下载数据集,但是后来觉得 ...
- 【模板】prufer序列
如何构造一个prufer序列? 我们给一棵无根树的节点编上号,每次找到一个编号最小的度为1节点,删除它,并输出与它连接的点的编号,直到只剩下两个节点. 这样,我们就构造出来了一个prufer序列. 通 ...
- 【POJ3744】Scout YYF I
Description YYF是一个英勇的侦查员.现在他正在执行打入到敌方内部的危险任务.在解决了一系列的险情后,YYF到达了敌方著名的"地雷路"起始点.这条路非常长,上面被精心排 ...