一、快速报错机制(fail-fast)

这是《Java编程思想》中关于快速报错机制的描述

Java容器有一种保护机制,能够防止多个进程同时修改同一个容器的内容。如果在你迭代遍历容器的过程中,另一个进程介入其中,并且插入、删除或者修改此容器内的某个对象,那么就会出现问题:也许迭代过程中已经处理过容器中的该元素了,也许还没处理,也许在调用size()之后容器的尺寸收缩了——还有许多灾难情景。Java容器类类库采用快速报错(fail-fast)机制。它会探查容器上的任何除了你的进程所进行的操作以外的所有变化,一旦它发现其它进程修改了容器,就会立刻抛出ConcurrentModificationException异常。这就是“快速报错”的意思——即,不是使用复杂的算法在事后来检查问题。

——from《Java编程思想》p517

二、ArrayList中的快速报错机制分析

快速报错机制在容器中使用非常广泛,我们最常用的ArrayList就用到了快速报错机制。下面是ArrayList的迭代器源码。

    #ArrayList的Iterator源码分析

    public Iterator<E> iterator() {
return new Itr();
} /**
* An optimized version of AbstractList.Itr
*
* 覆盖了父类中AbstractList.Itr的实现(优化版)
*/
private class Itr implements Iterator<E> {
//下一个要返回元素的索引
int cursor; // index of next element to return
//最后一个要返回元素的索引,-1表示不存在
int lastRet = -1; // index of last element returned; -1 if no such
//记录期望的修改次数(用于保证迭代器在遍历过程中不会有对集合的修改操作(迭代器的自身的remove方法除外))
int expectedModCount = modCount; 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];
} 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();
}
} /**
* 检查修改次数
*/
final void checkForComodification() {
//实际的修改次数和期望的修改次数不匹配,则抛出并发修改异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

原来,ArrayList从其父类AbstractList继承了一个modCount属性,每当对ArrayList进行修改(add,remove,clear等)时,就会相应的modCount的值加1。而ArrayList中迭代器的实现类Itr也有一个expectedModCount属性。一旦调用iterator()方法来使用迭代器时,Itr类也就被初始化,expectedModCount就会被赋予一个与modCount相等的值。如果接下来进行遍历操作,则每次调用next()方法获取值时都会先进行修改检查(checkForComodification),也就是检查modCount和expectedModCount两个值是否相等。如果在遍历过程中进行了对集合的其它修改操作而使得modCount值发生变化,从而造成两者不等,就立即抛出ConcurrentModificationException。这不就是乐观锁的思想吗。这就是快速失败机制(fail-fast)的实现原理。

另外,我们注意到迭代器自身也提供了remove方法,但该方法并不会修改modCount的值,这是因为我们通常也会通过迭代遍历去删除某一个指定的元素,所以迭代器中自身提供了该remove方法,并保证该remove方法是安全的,而不希望我们在迭代时使用容器提供remove方法。

我们可以得出这样的结论:
一旦使用了迭代器,无论是否已经开始迭代,都不能在接下来的过程对容器进行修改操作,这里的修改指的是容器自身提供的add/remove等修改方法。但是可以使用迭代器自身提供的修改方法(通常只有remove)。

三、避免依赖快速报错机制

需要说明两点
①.虽然ConcurrentModificationException被译为并发修改异常,但这里的"并发",并非仅仅指的是多线程场景。
在单线程情况下:要确保Iterator遍历过程顺利完成,必须保证遍历过程中不更改集合的内容(Iterator的remove()方法除外)。

多线程情况下:如果要在多线程环境中,在迭代ArrayList的同时也要修改ArrayList,则可以使用
Collections.synchronizedList(List list)或者CopyOnWriteArrayList。其中CopyOnWriteArrayList是可以避免ConcurrentModificationException。

实际上CopyOnWriteArrayList、ConcurrentHashMap和CopyOnWriteArraySet都使用了可以避免ConcurrentModificationException的技术。

②.迭代器的快速失败机制无法得到保证,它不能保证一定发生,只是会尽最大努力抛出ConcurrentModificationException异常。

为什么不能保证一定发生呢?
其实原因很简单,再回到前面ArrayList的迭代器代码,我们注意到修改检查并非在同步下进行的,如果容器进行修改操作而导致modCount发生变化,由于可见性,迭代器可能会看到失效的modCount值,从而不会意识到已经发生修改。而这是一种设计上的权衡。

因此,为提高此类操作的正确性,我们不能依赖于该机制,而要使用上一条中提到的线程安全的容器。

四、CopyOnWriteArrayList不支持快速报错机制

ArrayList在迭代遍历的同时进行并发修改是会发生快速失败,因此需要使用同步保证迭代的安全性。但ArrayList对应的线程安全容器CopyOnWriteArrayList能在遍历的同时进行修改,而且未使用同步。

那么它是怎么实现的呢?实际上很简单,CopyOnWriteArrayList每次在进行修改操作时,都会新生成一个数组,然后在新数组上修改,然后替换原数组,而遍历操作则是直接操作原有数组。因为遍历和修改操作的目标都不一样,因此根本不会互相影响。

来看看源码。

    /**
* 返回迭代器
*
* 返回的迭代器提供了该迭代器被创建时列表的快照。
* 当移动迭代器时,不需要同步。
* 迭代器不支持remove方法
*/
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
} /**
* 内部迭代器的实现类
*/
private static class COWIterator<E> implements ListIterator<E> {
/**数组的快照*/
private final Object[] snapshot;
private int cursor; private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
} public boolean hasNext() {
return cursor < snapshot.length;
} public boolean hasPrevious() {
return cursor > 0;
} @SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
//在快照数组上进行迭代操作
return (E) snapshot[cursor++];
} @SuppressWarnings("unchecked")
public E previous() {
if (! hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
} public int nextIndex() {
return cursor;
} public int previousIndex() {
return cursor-1;
} /**
* 迭代器不支持remove/set/add方法。
* 总是抛出UnsupportedOperationException异常
*/
public void remove() {
throw new UnsupportedOperationException();
}
public void set(E e) {
throw new UnsupportedOperationException();
}
public void add(E e) {
throw new UnsupportedOperationException();
}
}

迭代器只用于迭代,不提供任何修改操作。这点恰好跟ArrayList相反。

public class Demo {

    public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
//添加元素0-4
for(int i=0;i<5;i++){
list.add(i+"");
}
System.out.println(list);//[0, 1, 2, 3, 4] //进行迭代
Iterator iterator = list.iterator();
while(iterator.hasNext()){
String num = (String) iterator.next();
//迭代时,删除3
if("3".equals(num)){
//iterator.remove();//iterator不支持修改方法(add,set,remove)
list.remove(num);//使用原容器的remove方法
};
}
System.out.println(list);//[0, 1, 2, 4]
}
}

五、练习(如何安全的使用迭代器,如何正确的删除容器中的元素)

程序功能:分别使用for,foreach,iterator来遍历(迭代)容器,然后删除其中的值为"傻强"这个元素。

public class TestTest {

    private List<String> list;

    /**
* 初始化操作
*/
@Before
public void setUp(){
list = new ArrayList<String>();
list.add("刘德华");
list.add("周润发");
list.add("傻强");
list.add("古天乐");
list.add("刘青云");
System.out.println(list);
} /**
* Demo1:使用for循环,删除元素
*/
@Test
public void testFor(){
for(int i=0;i<list.size();i++){
//删除傻强
if("傻强".equals(list.get(i))){
list.remove(i);
}
}
System.out.println(list);
} /**
* Demo2:使用foreach,删除元素【错误】
*/
@Test
public void testForeach(){
for (String s : list) {
//删除傻强
if("傻强".equals(s)){
list.remove(s);//使用容器自身的remove
}
}
System.out.println(list);
} /**
* Demo3:使用Iterator,调用Iterator的remove()删除元素
*/
@Test
public void testIterator(){
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String s= iterator.next();
//删除lisi
if("傻强".equals(s)){
iterator.remove();//使用迭代器的remove
}
}
System.out.println(list);
} /**
* Demo4:使用Iterator,调用集合自身的remove()删除元素【错误】
*/
@Test
public void testIterator2(){
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String s= iterator.next();
//删除傻强
if("傻强".equals(s)){
list.remove(s);//使用容器的remove
}
}
System.out.println(list);
} /**
* Demo5:获得iterator后进行了错误操作【错误】
*/
@Test
public void testIterator3(){
Iterator<String> iterator = list.iterator();
//这是错误的操作。获取迭代器后不能再调用容器的修改方法
list.add("这是错误的行为");
//这是允许的。
//iterator.remove(xx); while(iterator.hasNext()){
String s= iterator.next();
//删除傻强
if("傻强".equals(s)){
iterator.remove();//使用迭代器的remove
}
}
System.out.println(list);
}
}

结果:除了demo1和demo3正确,其它demo都会报ConcurrentModificationException异常。

面试题
1.什么是快速失败机制(fail-fast)?

2.ArrayList中快速失败机制的实现原理?

3.为什么不能依赖快速失败机制?
4.如何正确的删除容器中的元素?

快速失败/报错机制 - fail-fast的更多相关文章

  1. RedisCluster的rename机制失败报错,解决又是数据倾斜问题

    需求说明:spring session中的用户session更新是更新key的名字,所以对于key的操作时需要用newkey 替换oldkey value值只允许存在一个,这里用到rename就很合适 ...

  2. Windows Essentials Movie Maker 安装失败报错 ——问题解决

    Windows Essentials Movie Maker 安装失败报错: (软件包名: wlsetup-all.exe) 查到官方论坛给出了一些回复: https://social.technet ...

  3. refiling失败报错Invalid function: org-preserve-local-variables

    refiling失败报错Invalid function: org-preserve-local-variables,原因: elc,不太清楚 解决办法: 删除org??目录下的elc文件 https ...

  4. 使用cnpm i -S axios 遇到报错Install fail! Error: EISDIR: illegal operation on a directory, symlink..........的解决办法

    “今天本来想在cnpm 环境下安装axios,但是在安装axios的时候出现了一些问题.使用cnpm淘宝镜像库下载安装axios的时候报错 Install fail! Error: EISDIR: i ...

  5. PHP 操作MySQL时mysql_connect( )和Mysqli( )的两种报错机制

    刚开始使用PHP连接MySQL数据库的时候,如果数据库连接不成功或者,对MySQL数据库进行增删改查等操作的时候,SQL语句存在错误,而在执行PHP文件的时候,浏览器并不会抛出错误的原因,一般是空白显 ...

  6. 2018最新win10 安装tensorflow1.4(GPU/CPU)+cuda8.0+cudnn8.0-v6 + keras 安装CUDA失败 导入tensorflow失败报错问题解决

    原文作者:aircraft 原文链接:https://www.cnblogs.com/DOMLX/p/9747019.html 基本开发环境搭建 1. Microsoft Windows 版本 关于W ...

  7. FTP上传失败报错227 Entering Passive Model (222,111,8,111,10,40)

    昨天为了一个ftp问题折腾了一天.问题背景:原来有个接口涉及到上传文件,服务端更换了ftp服务器,我们这边需要刷新连接服务端的ip和端口配置,代码没动.联调环境和验收环境都测试通过,一到生产环境就歇菜 ...

  8. uni-app真机调试报错request:fail abort解决方法

    Android端真机调试访问本地接口数据时报错:request:fail abort 报错代码 onLoad: function(e) { uni.request({ url: 'http://loc ...

  9. node-sass 安装失败报错的原因及解决办法(整理)

    npm install 时偶尔遇到报错:没有安装python或node-sass 安装失败的问题,百度之后发现是被墙了,但根据百度的方法换了淘宝镜像和用了vpn都安装失败,最后发现原来是因为没有卸载之 ...

随机推荐

  1. Python描述符的使用

    Python描述符的使用 前言 作为一位python的使用者,你可能使用python有一段时间了,但是对于python中的描述符却未必使用过,接下来是对描述符使用的介绍 场景介绍 为了引入描述符的使用 ...

  2. February 27th, 2018 Week 9th Tuesday

    Great minds think alike. 英雄所见略同. If great minds really did think alike, then we would live in an unr ...

  3. [福大软工] Z班——个人技术博客评分

    个人技术博客 作业地址 https://edu.cnblogs.com/campus/fzu/SoftwareEngineering2015/homework/1070 作业要求 个人技术博客单次作业 ...

  4. 【Teradata】gtwglobal查看

    使用root登录TD数据库节点 cnsterm 6 start gtwglobal cnsterm 3 //上一个命令的window编号 h //帮助 QUIT //退出

  5. 【JSOI2018】潜入行动

    [JSOI2018]潜入行动 树形\(DP\).设\(f_{i,j,0/1,0/1}\)表示以\(i\)为根的子树中,用了\(j\)个监听器,是否放置了监听器,是否被监听的方案数.转移就多讨论几种情况 ...

  6. 【vue】vue +element 搭建项目,this.$nextTick用法

    相关资料:https://www.cnblogs.com/leaf930814/p/7247478.html https://www.cnblogs.com/duanyue/p/7458340.htm ...

  7. 004_后端js编写工具

    一.框架同事用的后端调试用的工具 google=>"webpack dev server" https://webpack.github.io/docs/webpack-de ...

  8. [教程]教你如何制作彩色的3D打印Groot

    http://mc.dfrobot.com.cn/forum.php?mod=viewthread&tid=24916 准备工作: <ignore_js_op>  3D打印高精度G ...

  9. 分区默认segment大小变化(64k—>8M)

    _partition_large_extents和_index_partition_large_extents 参考: http://www.xifenfei.com/2013/08/%E5%88%8 ...

  10. [TPYBoard - Micropython之会python就能做硬件 8] 学习使用超声波模块制作避障小车

    转载请注明:@小五义 http://www.cnblogs.com/xiaowuyi 欢迎加入讨论群 64770604   一.实验器材 1.TPYboard V102板  一块 2.电机驱动模块L2 ...