【Java深入研究】6、fail-fast机制

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

在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. * @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. }

运行结果:

[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. 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 中定义的,为全局变量:

[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. 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机制。请看:

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

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

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也是相当有损耗的。

时间: 2024-10-12 17:33:55

【Java深入研究】6、fail-fast机制的相关文章

Java面试准备之JVM详细研究三(类加载机制)

类加载过程 一个类从编写完成后,编译为字节码之后,它要装载进内存有七个阶段: 加载 => (验证-> 准备-> 解析)=> 初始化=> 使用=> 卸载 括号中的三个步骤可以整合成为 “连接”步骤.其中的步骤并不是一个阶段结束,一个阶段才开始的.只是说他们的开始阶段基本遵循此顺序(解析阶段更是可能在使用的时候才发生,目的是配合动态绑定),这些阶段都是互相交叉的混合式进行的,通常会在一个阶段执行过程中调用或激活另一个阶段. 1.加载 ”加载“的过程是”类加载“过程的一个阶段

Java内存区域划分和GC机制

Java 内存区域和GC机制 目录 Java垃圾回收概况 Java内存区域 Java对象的访问方式 Java内存分配机制 Java GC机制 垃圾收集器 Java垃圾回收概况 Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代 码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢.这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制.概括地说,该机制对 JVM

Java内存组成和垃圾回收机制

眼看就要到找工作的时候了,平时在实验室也做了不少项目,可到头来,假设面试官问我平时做过什么,我确不知从何说起,也可以说我不知道说什么.前辈们早就说过,计算机这个行业需要不断的学习,也需要不断的积累,自问平时遇到过不少问题,也解决了不少问题,可到头来,好像都没什么印象了!在准备找工作的时候,就将平时一些研究过的,倒腾过的重新记录下吧!由于本人是第一次写博客,文笔不太好,内容可能也有很多借鉴了是前辈们的,但重在重新整理.精选,也让自己在整理的过程中重新学习,加深印象! 一.内存组成 在我做项目的时候

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

目录 Java GC系列 Java系列笔记(3) - Java 内存区域和GC机制 面试题:"你能不能谈谈,java GC是在什么时候,对什么东西,做了什么事情?" Java GC系列 本部分来自Java GC系列(1):Java垃圾回收简介 Java的内存分配与回收全部由JVM垃圾回收进程自动完成.与C语言不同,Java开发者不需要自己编写代码实现垃圾回收.这是Java深受大家欢迎的众多特性之一,能够帮助程序员更好地编写Java程序. 下面四篇教程是了解Java 垃圾回收(GC)的基

Java并发编程:Concurrent锁机制解析

.title { text-align: center } .todo { font-family: monospace; color: red } .done { color: green } .tag { background-color: #eee; font-family: monospace; padding: 2px; font-size: 80%; font-weight: normal } .timestamp { color: #bebebe } .timestamp-kwd

Java基础知识——类装载器与反射机制

类装载器ClassLoader 类装载器就是寻找类的字节码文件,并构造出类在JVM内部表示的对象组件. 类装载器把一个类装入JVM中,要经过三步: 1.装载:查找和导入Class文件: 2.链接:执行校验.准备和解析(解析是可以选择的): 3.初始化:对类的静态变量.静态代码块执行初始化工作: 类装载工作由ClassLoader及其子类负责.JVM在运行时会产生三个ClassLoader:根装载器.ExtClassLoader(扩展类装载器)和AppClassLoader(系统类装载器). 根装

完成这个例子,说出java中针对异常的处理机制。

有一个类为ClassA,有一个类为ClassB,在ClassB中有一个方法b,此方法抛出异常,在ClassA类中有一个方法a,请在这个方法中调用b,然后抛出异常.在客户端有一个类为TestC,有一个方法为c ,请在这个方法中捕捉异常的信息.完成这个例子,请说出java中针对异常的处理机制. [java] view plaincopy package com.itheima; import java.io.IOException; /** *第6题:有一个类为ClassA,有一个类为ClassB,

JAVA学习篇--ThreadLocal,Java中特殊的线程绑定机制

在DRP项目中,我们使用了ThreadLocal来创建Connection连接,避免了一直以参数的形式将Connection向下传递(传递connection的目的是由于jdbc事务要求确保使用同一个connection连接).那么ThreadLocal是如果做到的呢?它和同步锁的不同在哪里? 是什么: 对于ThreadLocal看英文单词我们很容易理解为一个线程的本地实现,但是它并不是一个Thread,而是threadlocalvariable(线程局部变量).也许把它命名为ThreadLoc

java注解研究

注解作用 常见的作用有以下几种: 生成文档.这是最常见的,也是java 最早提供的注解.常用的有@see @param @return @author等. 跟踪代码依赖性,实现替代配置文件功能.比较常见的是spring 2.5 开始的基于注解配置.作用就是减少配置.现在的框架基本都使用了这种配置来减少配置文件的数量.也是 在编译时进行格式检查.如@override 放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出. 包 java.lang.annotation 中包含所有定义自

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

前言 javascript事件基础 事件捕获/冒泡 事件对象 事件模拟 移动端响应速度 PC与移动端鼠标事件差异 touch与click响应速度问题 结论 zepto事件机制 注册/注销事件 zepto模拟tap事件 tap事件的问题一览 点透问题 fastclick思想提升点击响应 实现原理 鬼点击 ios与android鼠标事件差异 事件捕获解决鬼点击 结语 前言 这篇博客有点长,如果你是高手请您读一读,能对其中的一些误点提出来,以免我误人子弟,并且帮助我提高 如果你是javascript菜