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

例如,ArrayList:

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

HashMap中:

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

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

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

一、fail-fast示例

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

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

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

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

  1. protected transient int modCount = 0;

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

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

从上面的源代码我们可以看出,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机制。请看:

  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. }

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

  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. final void setArray(Object[] paramArrayOfObject) {
  17. this.array = paramArrayOfObject;
  18. }

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

  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://blog.csdn.net/chenssy/article/details/38151189

【Java深入研究】4、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. 转 Java虚拟机5:Java垃圾回收(GC)机制详解

    转 Java虚拟机5:Java垃圾回收(GC)机制详解 Java虚拟机5:Java垃圾回收(GC)机制详解 哪些内存需要回收? 哪些内存需要回收是垃圾回收机制第一个要考虑的问题,所谓“要回收的垃圾”无 ...

  3. fail fast和fail safe策略

    优先考虑出现异常的场景,当程序出现异常的时候,直接抛出异常,随后程序终止 import java.util.ArrayList; import java.util.Collections; impor ...

  4. 快速失败(fail—fast)和 安全失败(fail—safe)

    快速失败(fail-fast) 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的结构进行了修改(增加.删除),则会抛出Concurrent Modification Exception. 原理 ...

  5. 【转】Java之 内存区域和GC机制

    转自:Leo Chin 目录 Java垃圾回收概况 Java内存区域 Java对象的访问方式 Java内存分配机制 Java GC机制 垃圾收集器 Java垃圾回收概况 Java GC(Garbage ...

  6. Java内存区域划分和GC机制

    Java 内存区域和GC机制   目录 Java垃圾回收概况 Java内存区域 Java对象的访问方式 Java内存分配机制 Java GC机制 垃圾收集器 Java垃圾回收概况 Java GC(Ga ...

  7. java内存管理和gc回收机制

    Java垃圾回收概况 Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码 ...

  8. 深入理解Java虚拟机(自动内存管理机制)

    文章首发于公众号:BaronTalk 书籍真的是常读常新,古人说「书读百遍其义自见」还是很有道理的.周志明老师的这本<深入理解 Java 虚拟机>我细读了不下三遍,每一次阅读都有新的收获, ...

  9. Java GC(垃圾回收)机制知识总结

    目录 Java GC系列 Java关键术语 Java HotSpot 虚拟机 JVM体系结构 Java堆内存 启动Java垃圾回收 Java垃圾回收过程 垃圾回收中实例的终结 对象什么时候符合垃圾回收 ...

  10. 【移动端兼容问题研究】javascript事件机制详解(涉及移动兼容)

    前言 这篇博客有点长,如果你是高手请您读一读,能对其中的一些误点提出来,以免我误人子弟,并且帮助我提高 如果你是javascript菜鸟,建议您好好读一读,真的理解下来会有不一样的收获 在下才疏学浅, ...

随机推荐

  1. vue操作的填坑之旅

    1.渲染时图片存在,但损坏,会出现破损图,界面很丑呀 解决方案:调用vue显示的图片的时候会有一些图片404的问题,为了界面的美观,这里进行图片错误是显示默认图片的处理,下面贴出代码 data: { ...

  2. 跨域访问问题js

    您可以通过使用JSONP形式的回调函数来加载其他网域的JSON数据 访问百度的地址解析  返回来省,地区,市 $.getJSON("http://api.map.baidu.com/clou ...

  3. 任务调度及远端管理(基于Quartz.net)

    这篇文章我们来了解一些项目中的一个很重要的功能:任务调度 可能有些同学还不了解这个,其实简单点说任务调度与数据库中的Job是很相似的东西 只不过是运行的物理位置与管理方式有点不一样,从功能上来说我觉得 ...

  4. Javascript高级编程学习笔记(8)—— 变量

    日常更新~~ 变量 所有的编程语言中,变量都是赋予语言灵活性的根本所在. 那么JS中的变量又有那些与众不同的地方呢.? 按照ECMA-262的定义,JS的变量和其他编程语言的变量有很大的区别 其松散类 ...

  5. JavaScript实现LUHN算法验证银行卡号有效性

    一般验证银行卡有效性用到一种叫做LUHN的算法,简介请参考这篇博客:基于Luhn算法的银行卡卡号的格式校验 注意: 1.LUHN算法只是能校验卡号是否有效,并不能校验卡号和用户名是否一致. 2.如果有 ...

  6. Qt之使用CQU库快速开发统一风格界面

    在使用Qt开发时,肯定是想让开发的项目界面统一风格:不希望每个界面都要程序员用代码去修饰美化以及进行事件处理等等,这样非常繁琐,容易出错而且没有格调:所以我就开发一个动态链接库,封装统一的风格界面.事 ...

  7. Introduction to CELP Coding

    Speex is based on CELP, which stands for Code Excited Linear Prediction. This section attempts to in ...

  8. rsync实现目录同步

    rsync rsync是linux系统下的数据镜像备份工具.使用快速增量备份工具Remote Sync可以远程同步,支持本地复制,或者与其他SSH.rsync主机同步. 外文名 rsync 全     ...

  9. CentOS 部署 NodeBB

    0x00 前言 NodeBB 是基于 NodeJS 的开源 BBS 系统,可以搭配 redis 或 mongodb 数据库,本文中由于使用 NodeBB 配置中默认的 npm 安装会引起不知原因的 5 ...

  10. bus总线

    有时候两个组件也需要通信(非父子关系).当然Vue2.0提供了Vuex,但在简单的场景下,可以使用一个空的Vue实例作为中央事件总线. eventBus.js放在assets import Vue f ...