Java提高篇(三四)-----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示例

public class FailFastTest {    private static List<Integer> list = new ArrayList<>();        /**     * @desc:线程one迭代list     * @Project:test     * @file:FailFastTest.java     * @Authro:chenssy     * @data:2014年7月26日     */    private static class threadOne extends Thread{        public void run() {            Iterator<Integer> iterator = list.iterator();            while(iterator.hasNext()){                int i = iterator.next();                System.out.println("ThreadOne 遍历:" + i);                try {                    Thread.sleep(10);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }    }        /**     * @desc:当i == 3时,修改list     * @Project:test     * @file:FailFastTest.java     * @Authro:chenssy     * @data:2014年7月26日     */    private static class threadTwo extends Thread{        public void run(){            int i = 0 ;             while(i < 6){                System.out.println("ThreadTwo run:" + i);                if(i == 3){                    list.remove(i);                }                i++;            }        }    }        public static void main(String[] args) {        for(int i = 0 ; i < 10;i++){            list.add(i);        }        new threadOne().start();        new threadTwo().start();    }}

运行结果:

ThreadOne 遍历:0ThreadTwo run:0ThreadTwo run:1ThreadTwo run:2ThreadTwo run:3ThreadTwo run:4ThreadTwo run:5Exception in thread "Thread-0" java.util.ConcurrentModificationException    at java.util.ArrayList$Itr.checkForComodification(Unknown Source)    at java.util.ArrayList$Itr.next(Unknown Source)    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中迭代器的源代码:

private class Itr implements Iterator<E> {        int cursor;        int lastRet = -1;        int expectedModCount = ArrayList.this.modCount;        public boolean hasNext() {            return (this.cursor != ArrayList.this.size);        }        public E next() {            checkForComodification();            /** 省略此处代码 */        }        public void remove() {            if (this.lastRet < 0)                throw new IllegalStateException();            checkForComodification();            /** 省略此处代码 */        }        final void checkForComodification() {            if (ArrayList.this.modCount == this.expectedModCount)                return;            throw new ConcurrentModificationException();        }    }

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

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

protected transient int modCount = 0;

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

    public boolean add(E paramE) {        ensureCapacityInternal(this.size + 1);        /** 省略此处代码 */    }    private void ensureCapacityInternal(int paramInt) {        if (this.elementData == EMPTY_ELEMENTDATA)            paramInt = Math.max(10, paramInt);        ensureExplicitCapacity(paramInt);    }        private void ensureExplicitCapacity(int paramInt) {        this.modCount += 1;    //修改modCount        /** 省略此处代码 */    }       public boolean remove(Object paramObject) {        int i;        if (paramObject == null)            for (i = 0; i < this.size; ++i) {                if (this.elementData[i] != null)                    continue;                fastRemove(i);                return true;            }        else            for (i = 0; i < this.size; ++i) {                if (!(paramObject.equals(this.elementData[i])))                    continue;                fastRemove(i);                return true;            }        return false;    }    private void fastRemove(int paramInt) {        this.modCount += 1;   //修改modCount        /** 省略此处代码 */    }    public void clear() {        this.modCount += 1;    //修改modCount        /** 省略此处代码 */    }

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

private static class COWIterator<E> implements ListIterator<E> {        /** 省略此处代码 */        public E next() {            if (!(hasNext()))                throw new NoSuchElementException();            return this.snapshot[(this.cursor++)];        }        /** 省略此处代码 */    }

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

public boolean add(E paramE) {        ReentrantLock localReentrantLock = this.lock;        localReentrantLock.lock();        try {            Object[] arrayOfObject1 = getArray();            int i = arrayOfObject1.length;            Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);            arrayOfObject2[i] = paramE;            setArray(arrayOfObject2);            int j = 1;            return j;        } finally {            localReentrantLock.unlock();        }    }        final void setArray(Object[] paramArrayOfObject) {        this.array = paramArrayOfObject;    }

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

Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);arrayOfObject2[i] = paramE;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


-----原文出自:http://cmsblogs.com/?p=1220,请尊重作者辛勤劳动成果,转载说明出处.

-----个人站点:http://cmsblogs.com

PS:如果你觉得文章对你有所帮助,别忘了 推荐或者 分享,因为有你的支持,才是我续写下篇的动力和源泉!
  • 作者: chenssy 
    出处: http://www.cnblogs.com/chenssy/ 
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

分类:  【java开发】---java提高篇
好文要顶  关注我  收藏该文   

chenssy
关注 - 35
粉丝 - 1495

+加关注

1
3

« 上一篇: Java提高篇(三三)-----Map总结
» 下一篇: Java提高篇(三五)-----Java集合细节(一):请为集合指定初始容量

posted @  2014-07-26 14:45  chenssy 阅读( 2005) 评论( 9)  编辑  收藏
评论列表
#1楼   2014-07-29 09:28  asonga 
有个问题请教一下,vector 是现行安全的, 但是我按照你的方法测试,也是报 java.util.ConcurrentModificationException,什么原因呢,那vector的所谓线性安全体现在哪呢
支持(0) 反对(0)

#2楼   2014-07-30 23:48  leoric xu 
@ asonga
正解,各种原因等楼主自己去挖吧哈哈!
支持(0) 反对(0)

#3楼 [ 楼主]  2014-07-31 09:24  chenssy 
@ asonga
线程安全就不会抛出ConcurrentModificationException?看Iterator的源码就可以发现,它迭代过程是不允许出现对集合进行修改的现象的。
支持(1) 反对(1)

#4楼 [ 楼主]  2014-07-31 09:24  chenssy 
@ leoric xu
有些东西自己去挖可能收获更大
支持(0) 反对(0)

#5楼   2014-07-31 21:07  leoric xu 
@ chenssy
1楼只是想告诉你,你的方案1说的不对而已
支持(0) 反对(0)

#6楼   2015-05-30 11:30  小鱼也要飞 
@ asonga
可不可以这样理解,虽然vecotr为同步容器,但是在多线程环境下面还是会隐藏问题。
支持(0) 反对(0)

#7楼   2015-09-07 16:09  云尸兄 
遍历时作修改集合结构就用Iterator
支持(0) 反对(0)

#8楼   2016-09-22 14:51  DreamerWorld 
方案1 明显不对,这个fast-fail机制,是为了保证,在集合遍历过程中,不允许去修改增删集合的。
支持(0) 反对(0)

#9楼 [ 楼主]  2016-09-26 09:06  chenssy 
@ DreamerWorld 
可以!我在遍历、增加、修改、删除的方法上面加锁了,为何不可以?

Java提高篇(三四)-----fail-fast机制相关推荐

  1. Java - Java集合中的快速失败Fail Fast 机制

    文章目录 什么是 fail-fast 源码解读 Itr 为什么对集合的结构进行修改会发生并发修改异常-源码分析 修改方法之 remove 修改方法之 add 案例分享 [案例一] [案例二] [案例三 ...

  2. java提高篇(三十)-----Iterator

    本文转载地址:            http://blog.csdn.net/chenssy/article/details/37521461 迭代对于我们搞Java的来说绝对不陌生.我们常常使用J ...

  3. java提高篇之抽象类与接口

    转载自 java提高篇之抽象类与接口 接口和内部类为我们提供了一种将接口与实现分离的更加结构化的方法. 抽象类与接口是java语言中对抽象概念进行定义的两种机制,正是由于他们的存在才赋予java强大的 ...

  4. java迭代器cas,java提高篇(三十)-Iterator - Java 技术驿站-Java 技术驿站

    迭代对于我们搞Java的来说绝对不陌生.我们常常使用JDK提供的迭代接口进行Java集合的迭代. Iterator iterator = list.iterator(); while(iterator ...

  5. java提高篇(三)-----理解java的三大特性之多态

    面向对象编程有三大特性:封装.继承.多态. 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据.对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法. 继承 ...

  6. Java提高篇——Java实现多重继承

    多重继承指的是一个类可以同时从多于一个的父类那里继承行为和特征,然而我们知道Java为了保证数据安全,它只允许单继承.有些时候我们会认为如果系统中需要使用多重继承往往都是糟糕的设计,这个时候我们往往需 ...

  7. 【转】java提高篇(十)-----详解匿名内部类

    原文网址:http://www.cnblogs.com/chenssy/p/3390871.html 在java提高篇-----详解内部类中对匿名内部类做了一个简单的介绍,但是内部类还存在很多其他细节 ...

  8. java提高篇(九)-----实现多重继承

    多重继承指的是一个类可以同时从多于一个的父类那里继承行为和特征,然而我们知道Java为了保证数据安全,它只允许单继承.有些时候我们会认为如果系统中需要使用多重继承往往都是糟糕的设计,这个时候我们往往需 ...

  9. java提高篇(八)----详解内部类

    可以将一个类的定义放在另一个类的定义内部,这就是内部类. 内部类是一个非常有用的特性但又比较难理解使用的特性(鄙人到现在都没有怎么使用过内部类,对内部类也只是略知一二). 第一次见面 内部类我们从外面 ...

最新文章

  1. 2021牛客寒假算法基础集训营1 J 一群小青蛙呱蹦呱蹦呱
  2. 差异表达基因富集结果可视化
  3. 【Harvest源码分析】GetF0CandidateContour函数
  4. IDEA 中比较骚后缀补全技巧!你可能没用过
  5. 云计算军事运用有啥特点
  6. 数学/找规律/sgu 118 Digital root
  7. 用devc++表白_教你用C语言加图形库打造炫酷表白连连看
  8. 算法(9)--两个数的最大公约数
  9. PACKING【二维01背包】
  10. linux 中常见的压缩和解压缩的命令
  11. eclipse中maven项目pom文件第一行报错解决方法
  12. Demo(3月28日)
  13. 《AutoCAD 2013中文版从入门到精通》—— 导读
  14. 【三维地图】开发攻略 —— 详解“GeoJSON”技术和应用场景
  15. DS8880要将双控高端存储架构进行到底
  16. 如何开发手机商城app? 商城APP功能
  17. Prometheus+Grafana搭建Jmeter性能监控平台
  18. 在苹果 M1 上运行 Linux 虚拟机变得容易了
  19. 易基因|全基因组cfDNA甲基化分析提高了早期乳腺癌无创诊断成像的准确性
  20. 集线器、中继器、网桥、交换机、网关、路由器——今天必把你们区分开

热门文章

  1. VR插件ViveInputUtility---简单介绍
  2. 重装windows后如何找回我的ubuntu
  3. 微信加入“微社区” Discuz!发力移动社交
  4. 光场1.0——非聚焦型光场相机
  5. 签三方、试用期 注意事项
  6. mysql 表的crud_MySQL中表的数据的增删改查(CRUD)
  7. c语言除法加速,干货!C语言高效编程与代码优化
  8. 计算从某个日期开始往前或往后天数的日期
  9. 6-8 连接字符串 (15 分)
  10. 论文编辑中wps问题和技巧