转自: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. draw call 理解和优化

    draw call是openGL的描绘次数(directX没怎么研究,但原理应该差不多)一个简单的openGL的绘图次序是:设置颜色→绘图方式→顶点座标→绘制→结束.每帧都会重复以上的步骤.这就是一次 ...

  2. 关于华为x1 7.0无法从eclipse发布的更新as发布的apk

    目前只在华为x1 7.0手机上发现这个问题,坑大了. MediaPad 10 FHD 华为这款可以安装. HUAWEI G525-U00 华为这款也可以安装. 目前公司就这几款华为手机了. 原因是 在 ...

  3. esriControlsMousePointer常量

    控制鼠标指针选项. 不变 值 描述 esriPointerParentWindow -1 指针指定的父窗口或窗体. esriPointerDefault 0 默认指针,箭头一样. esriPointe ...

  4. 在go中使用leveldb --levi

    github上有个比较好用的leveldb go wrapperlevigo, 安装之前需现在机器上安装leveldb 当前版本的LevelDB没有带安装脚本,需自行编译安装,过程如下: instal ...

  5. LaTeX 制作表格

    实例代码: \begin{table}[h] \centering \begin{tabular}{|c|c|} \hline \textbf{Distribution} & \textbf{ ...

  6. 两周赚100万 在Google Play做对了什么

    http://tech.it168.com/a2013/0301/1457/000001457913_all.shtml

  7. 使用vs远程调试iis站点

    在vs安装目录下IDE文件夹下的Remote Debugger 复制到服务器运行 启动msvsmon.exe msvsmon.exe启动后设置远程连接不验证身份 vs中 调试→附加到进程 ip+端口访 ...

  8. WCF公开服务元数据方式

    一般我们使用了scvutil命令自动生成了服务的客户端代理类: 例如:svcutil http://localhost:8000/?wsdl /o:FirstServiceClient.cs 命令中h ...

  9. python 去掉字符串的 "

    list_name = ["hello", "岚", "许言午", "公司", "赵六", &quo ...

  10. 点滴积累【JS】---JS小功能(JS实现排序)

    效果: 思路: 首先,获得用到的ID,在把得到的<li>数组添加到array数组里面,然后在进行array排序,排序完后再将array中的数据用appendChild添加到ul里面: 代码 ...