转自:http://blog.csdn.net/chenssy/article/details/38151189

2014-07-26 22:40 8219人阅读 评论(22) 收藏 举报
 分类:
【JAVA开发】-----Java提高篇(36) 

版权声明:本文为博主原创文章,未经博主允许不得转载。

 

目录(?)[+]

 

在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://www.cnblogs.com/skywang12345/p/3308762.html#a3

(转)java 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. fail fast和fail safe策略

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

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

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

  4. Java 动态代理机制分析及扩展

    Java 动态代理机制分析及扩展,第 1 部分 王 忠平, 软件工程师, IBM 何 平, 软件工程师, IBM 简介: 本文通过分析 Java 动态代理的机制和特点,解读动态代理类的源代码,并且模拟 ...

  5. Java中JIN机制及System.loadLibrary() 的执行过程

    Android平台Native开发与JNI机制详解 http://mysuperbaby.iteye.com/blog/915425 个人认为下面这篇转载的文章写的很清晰很不错. 注意Android平 ...

  6. Java 动态代理机制分析及扩展--转

    http://www.ibm.com/developerworks/cn/java/j-lo-proxy1/#icomments http://www.ibm.com/developerworks/c ...

  7. Java 动态代理机制分析及扩展,第 1 部分

    Java 动态代理机制分析及扩展,第 1 部分 http://www.ibm.com/developerworks/cn/java/j-lo-proxy1/ 本文通过分析 Java 动态代理的机制和特 ...

  8. 深入理解 Java 中 SPI 机制

    本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/vpy5DJ-hhn0iOyp747oL5A作者:姜柱 SPI(Service Provider ...

  9. Java的多线程机制系列:不得不提的volatile及指令重排序(happen-before)

    一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ...

  10. java的锁机制

    一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在Java里边就是拿到某个同步对象的锁(一个对象只有一把锁): 如果这个时候同步对象的锁被其他线程拿走了,他(这个线 ...

随机推荐

  1. postgresql 如何导入sql文件

    (1)不能使用pgadmin 执行Copy语句,目前客户端还不支持这种写法. (2)打开sql shell,执行如下操作 \i C:/Users/Peter_Youny/Desktop/ischool ...

  2. hdu1700 Points on Cycle (数学)

    Problem Description There is a cycle with its center on the origin. Now give you a point on the cycl ...

  3. 用开源NGINX-RTMP-MODULE搭建FLASH直播环境

    用开源nginx-rtmp-module搭建flash直播环境 1.将nginx和nginx-rtmp-module的源码包解压PS:nginx-rtmp-module网址https://github ...

  4. 安卓Eclipse开发人员的福音

    我们知道.谷歌已经放弃对Eclipse(ADT)的维护更新了.如今官网上也找不到ADT的下载链接了,我们大多数同学仍在使用的ADT版本号可能已经非常老了,预计大多数的SDK版本号仅仅到4.4,而,在尝 ...

  5. VS报表图解《一》---菜鸟版

    与原先的开发环境VB.EXE不同VS2013自带了报表控件ReportViewer能够内部实现报表的设计,本文主要通过绑定数据集来实现报表的显示 1.加入:reportviewer控件,当将控件显示在 ...

  6. webDriver API——第6部分Locate elements By

    These are the attributes which can be used to locate elements. See the Locating Elements chapter for ...

  7. KVM Run Process之KVM核心流程

    在"KVM Run Process之Qemu核心流程"一文中讲到Qemu通过KVM_RUN调用KVM提供的API发起KVM的启动,从这里进入到了内核空间执行,本文主要讲述内核中KV ...

  8. Nunit2.5.10快速上手(笔记)

    1.下载Nunit:http://www.nunit.org/index.php?p=download,下载MSI格式的安装包: 2.安装Nunit,根据提示安装即可,没有什么需要配置的,直接下一步就 ...

  9. sql替换字符串部分内容

    update 表名 set 字段名=replace(cast(与前面一样的字段名 as varchar(8000)) ,'原本内容','想要替换成什么') update News set news_d ...

  10. IOS 通过界面图标启动Web应用 + 全屏应用 + 添加到主屏幕

    请注意!!!使用了[全屏模式之后].页面的顶部会空出一大块.而且这并不属于margin,padding,或者定位.就是单纯的空出来非常难调试.其实坑就是这里 在 iPhone「添加到主屏幕」时显示自定 ...