`

集合的fail-fast 转载

 
阅读更多

  在JDK的Collection中我们时常会看到类似于这样的话:

        例如,ArrayList:

注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测 bug。

        HashMap中:

注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。

        在这两段话中反复地提到”快速失败”。那么何为”快速失败”机制呢?

        “快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

一、fail-fast示例

[java] view plain copy
 
  1. public class FailFastTest {  
  2.     private static List<Integer> list = new ArrayList<>();  
  3.       
  4.     /** 
  5.      * @desc:线程one迭代list 
  6.      * @Project:test 
  7.      * @file:FailFastTest.java 
  8.      * @Authro:chenssy 
  9.      * @data:2014年7月26日 
  10.      */  
  11.     private static class threadOne extends Thread{  
  12.         public void run() {  
  13.             Iterator<Integer> iterator = list.iterator();  
  14.             while(iterator.hasNext()){  
  15.                 int i = iterator.next();  
  16.                 System.out.println("ThreadOne 遍历:" + i);  
  17.                 try {  
  18.                     Thread.sleep(10);  
  19.                 } catch (InterruptedException e) {  
  20.                     e.printStackTrace();  
  21.                 }  
  22.             }  
  23.         }  
  24.     }  
  25.       
  26.     /** 
  27.      * @desc:当i == 3时,修改list 
  28.      * @Project:test 
  29.      * @file:FailFastTest.java 
  30.      * @Authro:chenssy 
  31.      * @data:2014年7月26日 
  32.      */  
  33.     private static class threadTwo extends Thread{  
  34.         public void run(){  
  35.             int i = 0 ;   
  36.             while(i < 6){  
  37.                 System.out.println("ThreadTwo run:" + i);  
  38.                 if(i == 3){  
  39.                     list.remove(i);  
  40.                 }  
  41.                 i++;  
  42.             }  
  43.         }  
  44.     }  
  45.       
  46.     public static void main(String[] args) {  
  47.         for(int i = 0 ; i < 10;i++){  
  48.             list.add(i);  
  49.         }  
  50.         new threadOne().start();  
  51.         new threadTwo().start();  
  52.     }  
  53. }  
 运行结果:

 

[java] view plain copy
 
  1. ThreadOne 遍历:0  
  2. ThreadTwo run:0  
  3. ThreadTwo run:1  
  4. ThreadTwo run:2  
  5. ThreadTwo run:3  
  6. ThreadTwo run:4  
  7. ThreadTwo run:5  
  8. Exception in thread "Thread-0" java.util.ConcurrentModificationException  
  9.     at java.util.ArrayList$Itr.checkForComodification(Unknown Source)  
  10.     at java.util.ArrayList$Itr.next(Unknown Source)  
  11.     at test.ArrayListTest$threadOne.run(ArrayListTest.java:23)  

 

二、fail-fast产生原因

        通过上面的示例和讲解,我初步知道fail-fast产生的原因就在于程序在对 collection 进行迭代时,某个线程对该 collection 在结构上对其做了修改,这时迭代器就会抛出 ConcurrentModificationException 异常信息,从而产生 fail-fast。

        要了解fail-fast机制,我们首先要对ConcurrentModificationException 异常有所了解。当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常。同时需要注意的是,该异常不会始终指出对象已经由不同线程并发修改,如果单线程违反了规则,同样也有可能会抛出改异常。

        诚然,迭代器的快速失败行为无法得到保证,它不能保证一定会出现该错误,但是快速失败操作会尽最大努力抛出ConcurrentModificationException异常,所以因此,为提高此类操作的正确性而编写一个依赖于此异常的程序是错误的做法,正确做法是:ConcurrentModificationException 应该仅用于检测 bug。下面我将以ArrayList为例进一步分析fail-fast产生的原因。

从前面我们知道fail-fast是在操作迭代器时产生的。现在我们来看看ArrayList中迭代器的源代码:

 

[java] view plain copy
 
  1. private class Itr implements Iterator<E> {  
  2.         int cursor;  
  3.         int lastRet = -1;  
  4.         int expectedModCount = ArrayList.this.modCount;  
  5.   
  6.         public boolean hasNext() {  
  7.             return (this.cursor != ArrayList.this.size);  
  8.         }  
  9.   
  10.         public E next() {  
  11.             checkForComodification();  
  12.             /** 省略此处代码 */  
  13.         }  
  14.   
  15.         public void remove() {  
  16.             if (this.lastRet < 0)  
  17.                 throw new IllegalStateException();  
  18.             checkForComodification();  
  19.             /** 省略此处代码 */  
  20.         }  
  21.   
  22.         final void checkForComodification() {  
  23.             if (ArrayList.this.modCount == this.expectedModCount)  
  24.                 return;  
  25.             throw new ConcurrentModificationException();  
  26.         }  
  27.     }  

 

        从上面的源代码我们可以看出,迭代器在调用next()、remove()方法时都是调用checkForComodification()方法,该方法主要就是检测modCount == expectedModCount ? 若不等则抛出ConcurrentModificationException 异常,从而产生fail-fast机制。所以要弄清楚为什么会产生fail-fast机制我们就必须要用弄明白为什么modCount != expectedModCount ,他们的值在什么时候发生改变的。

        expectedModCount 是在Itr中定义的:int expectedModCount = ArrayList.this.modCount;所以他的值是不可能会修改的,所以会变的就是modCount。modCount是在 AbstractList 中定义的,为全局变量:

 

[java] view plain copy
 
  1. protected transient int modCount = 0;  

 

那么他什么时候因为什么原因而发生改变呢?请看ArrayList的源码:

 

[java] view plain copy
 
  1. public boolean add(E paramE) {  
  2.     ensureCapacityInternal(this.size + 1);  
  3.     /** 省略此处代码 */  
  4. }  
  5.   
  6. private void ensureCapacityInternal(int paramInt) {  
  7.     if (this.elementData == EMPTY_ELEMENTDATA)  
  8.         paramInt = Math.max(10, paramInt);  
  9.     ensureExplicitCapacity(paramInt);  
  10. }  
  11.   
  12. private void ensureExplicitCapacity(int paramInt) {  
  13.     this.modCount += 1;    //修改modCount  
  14.     /** 省略此处代码 */  
  15. }  
  16.   
  17. ublic boolean remove(Object paramObject) {  
  18.     int i;  
  19.     if (paramObject == null)  
  20.         for (i = 0; i < this.size; ++i) {  
  21.             if (this.elementData[i] != null)  
  22.                 continue;  
  23.             fastRemove(i);  
  24.             return true;  
  25.         }  
  26.     else  
  27.         for (i = 0; i < this.size; ++i) {  
  28.             if (!(paramObject.equals(this.elementData[i])))  
  29.                 continue;  
  30.             fastRemove(i);  
  31.             return true;  
  32.         }  
  33.     return false;  
  34. }  
  35.   
  36. private void fastRemove(int paramInt) {  
  37.     this.modCount += 1;   //修改modCount  
  38.     /** 省略此处代码 */  
  39. }  
  40.   
  41. public void clear() {  
  42.     this.modCount += 1;    //修改modCount  
  43.     /** 省略此处代码 */  
  44. }  

 

        从上面的源代码我们可以看出,ArrayList中无论add、remove、clear方法只要是涉及了改变ArrayList元素的个数的方法都会导致modCount的改变。所以我们这里可以初步判断由于expectedModCount 得值与modCount的改变不同步,导致两者之间不等从而产生fail-fast机制。知道产生fail-fast产生的根本原因了,我们可以有如下场景:

        有两个线程(线程A,线程B),其中线程A负责遍历list、线程B修改list。线程A在遍历list过程的某个时候(此时expectedModCount = modCount=N),线程启动,同时线程B增加一个元素,这是modCount的值发生改变(modCount + 1 = N + 1)。线程A继续遍历执行next方法时,通告checkForComodification方法发现expectedModCount  = N  ,而modCount = N + 1,两者不等,这时就抛出ConcurrentModificationException 异常,从而产生fail-fast机制。

        所以,直到这里我们已经完全了解了fail-fast产生的根本原因了。知道了原因就好找解决办法了。

三、fail-fast解决办法

        通过前面的实例、源码分析,我想各位已经基本了解了fail-fast的机制,下面我就产生的原因提出解决方案。这里有两种解决方案:

        方案一:在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。

        方案二:使用CopyOnWriteArrayList来替换ArrayList。推荐使用该方案。

        CopyOnWriteArrayList为何物?ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。 该类产生的开销比较大,但是在两种情况下,它非常适合使用。1:在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时。2:当遍历操作的数量大大超过可变操作的数量时。遇到这两种情况使用CopyOnWriteArrayList来替代ArrayList再适合不过了。那么为什么CopyOnWriterArrayList可以替代ArrayList呢?

        第一、CopyOnWriterArrayList的无论是从数据结构、定义都和ArrayList一样。它和ArrayList一样,同样是实现List接口,底层使用数组实现。在方法上也包含add、remove、clear、iterator等方法。

        第二、CopyOnWriterArrayList根本就不会产生ConcurrentModificationException异常,也就是它使用迭代器完全不会产生fail-fast机制。请看:

 

[java] view plain copy
 
  1. private static class COWIterator<E> implements ListIterator<E> {  
  2.         /** 省略此处代码 */  
  3.         public E next() {  
  4.             if (!(hasNext()))  
  5.                 throw new NoSuchElementException();  
  6.             return this.snapshot[(this.cursor++)];  
  7.         }  
  8.   
  9.         /** 省略此处代码 */  
  10.     }  

 

        CopyOnWriterArrayList的方法根本就没有像ArrayList中使用checkForComodification方法来判断expectedModCount 与 modCount 是否相等。它为什么会这么做,凭什么可以这么做呢?我们以add方法为例:

 

[java] view plain copy
 
  1. public boolean add(E paramE) {  
  2.         ReentrantLock localReentrantLock = this.lock;  
  3.         localReentrantLock.lock();  
  4.         try {  
  5.             Object[] arrayOfObject1 = getArray();  
  6.             int i = arrayOfObject1.length;  
  7.             Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);  
  8.             arrayOfObject2[i] = paramE;  
  9.             setArray(arrayOfObject2);  
  10.             int j = 1;  
  11.             return j;  
  12.         } finally {  
  13.             localReentrantLock.unlock();  
  14.         }  
  15.     }  
  16.   
  17.       
  18.     final void setArray(Object[] paramArrayOfObject) {  
  19.         this.array = paramArrayOfObject;  
  20.     }  

 

        CopyOnWriterArrayList的add方法与ArrayList的add方法有一个最大的不同点就在于,下面三句代码:

 

[java] view plain copy
 
  1. Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);  
  2. arrayOfObject2[i] = paramE;  
  3. setArray(arrayOfObject2);  

 

        就是这三句代码使得CopyOnWriterArrayList不会抛ConcurrentModificationException异常。他们所展现的魅力就在于copy原来的array,再在copy数组上进行add操作,这样做就完全不会影响COWIterator中的array了。

        所以CopyOnWriterArrayList所代表的核心概念就是:任何对array在结构上有所改变的操作(add、remove、clear等),CopyOnWriterArrayList都会copy现有的数据,再在copy的数据上修改,这样就不会影响COWIterator中的数据了,修改完成之后改变原有数据的引用即可。同时这样造成的代价就是产生大量的对象,同时数组的copy也是相当有损耗的。

 

        参考文档:http://www.cnblogs.com/skywang12345/p/3308762.html#a3

 

分享到:
评论

相关推荐

    【面试普通人VS高手系列】Fail-safe机制与Fail-fast机制分别有什么作用.doc

    这种机制下的集合容器,例如HashMap和ArrayList等,都是java.util包下的集合类,它们在遍历时直接访问集合内容,因此在遍历过程中对集合数据做变更时,就会发生Fail-fast。 Fail-safe机制是一种失败安全机制,在...

    前端开源库-hydro-fail-fast

    【前端开源库-hydro-fail-fast】是一个专为前端开发者设计的开源库,其核心功能是在测试过程中实现“液压快速失效”机制。这个概念源于工程领域,尤其在液压试验中,当系统首次检测到异常或故障时,为了保护设备和...

    Fail-Fast机制1

    Fail-Fast机制是一种Java集合框架中的设计策略,主要用于在多线程环境下确保程序的正确性和稳定性。当一个线程在遍历集合时,如果另一个线程对集合进行了结构上的修改,Fail-Fast机制会立即抛出`...

    一不小心就让Java开发踩坑的fail-fast是个什么鬼?(推荐)

    Java 集合类中的 fail-fast 机制是一种错误检测机制,当多个线程对集合进行结构上的改变时,可能会触发 fail-fast 机制,抛出 ConcurrentModificationException。 Fail-fast 机制的优点是可以预先识别出一些错误...

    fail-safe fail-fast知多少

    如果在遍历过程中,集合的结构被修改(例如添加、删除元素或改变元素位置),Fail-fast Iterator会立即抛出`ConcurrentModificationException`。这是因为Fail-fast Iterator内部维护了一个叫做`modCount`的变量,...

    老生常谈java中的fail-fast机制

    Java中的Fail-Fast机制是一种错误检测机制,当多个线程对集合进行结构上的改变操作时,有可能会产生Fail-Fast机制。这种机制是Java集合框架的一部分,可以帮助检测并发修改引发的错误。 Fail-Fast机制的原理是,当...

    Fail-Operational Safety Architecture for ADAS system

    本文“基于域控制器的ADAS系统故障操作安全架构”(Fail-Operational Safety Architecture for ADAS Systems Considering Domain ECUs)由Bülent Sari 和 Hans-Christian Reuss共同撰写,详细探讨了如何构建高效的...

    cypress-fail-fast:赛普拉斯插件可在首次失败时跳过测试

    目录常见方案的配置示例并行运行的配置TypeScript的用法 安装将插件添加到devDependenciesnpm i --save-dev cypress-fail-fast 在cypress/plugins/index.js内部: module . exports = ( on , config ) =&gt; { require ...

    解析Java的迭代器中的fast-fail错误检测机制

    fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所...

    minitest-fail-fast:重新实现RSpec的“快速故障”功能以实现最小测试

    gem 'minitest-fail-fast' 然后执行: $ bundle 或将其自己安装为: $ gem install minitest-fail-fast 用法 您可以通过传递-f ,-- --fail-fast来激活插件: $ TESTOPTS="--fail-fast" bundle exec rake ...

    NPIV fail-over

    标题与描述:“NPIV fail-over” ### NPIV Fail-over 概述 NPIV(Network Port ID Virtualization)是一种在存储网络中实现虚拟化的技术,尤其在光纤通道(FC)环境中应用广泛。它允许单个物理端口承载多个虚拟...

    Architectural Concepts for Fail-Operational Automotive Systems.xdf

    Architectural Concepts for Fail-Operational Automotive Systems

    grunt-fail-fast-task-runner:在多个Grunt项目上运行任务的Grunt任务,这些任务很快就会失败

    npm install grunt-fail-fast-task-runner --save-dev 插件安装完成后,可以使用以下JavaScript代码在您的Gruntfile中启用该插件: grunt . loadNpmTasks ( 'grunt-fail-fast-task-runner' ) ; “ fail_fast_task_...

    Checkpointing Workflows for Fail-Stop Errors.docx

    ### Checkpointing Workflows for Fail-Stop Errors #### 关键知识点概述 本报告由Yves Robert教授撰写,主要内容是关于如何在可能发生失败停止错误(fail-stop errors)的并行计算平台上,有效地调度工作流应用程序...

    A Practical Fail-Operational steering concept.pdf

    文章中提出的“Practical Fail-Operational Steering Concept”正是针对高度自动驾驶车辆面临的一个关键问题——在系统故障时如何保障转向系统的持续操作性,并通过硬件冗余实现。 首先,冗余的概念是指为系统的...

    前端开源库-webpack-fail-plugin

    **Webpack-Fail-Plugin详解** Webpack 是前端开发中不可或缺的模块打包工具,它负责将JavaScript、CSS、图片等资源进行优化、合并,并将其打包成一个或多个可部署的静态资源。然而,在构建过程中,如果出现错误,...

    【Java基础】集合框架-面试题.pdf

    【Java基础】集合框架-面试题。包含: 1. ArrayList 和 Vector 的区别;...3. 快速失败 (fail-fast) 和安全失败 (fail-safe) 的区别; 4. HashMap 的数据结构、工作原理 等Java集合部分经常遇到的面试题总结

    jest-fail-on-console:使用console.error()或console.warn()进行笑话测试的实用程序失败

    yarn add -D jest-fail-on-console 或者 npm install -D jest-fail-on-console 如何使用 在Jest的setupFilesAfterEnv选项中使用的文件中,添加以下代码: import failOnConsole from 'jest-fail-on-console' ...

Global site tag (gtag.js) - Google Analytics