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

}

运行结果:

ThreadOne 遍历:0
ThreadTwo run:0
ThreadTwo run:1
ThreadTwo run:2
ThreadTwo run:3
ThreadTwo run:4
ThreadTwo run:5
Exception 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;
    </span><span style="color: #0000ff">public</span> <span style="color: #0000ff">boolean</span><span style="color: #000000"> hasNext() {
</span><span style="color: #0000ff">return</span> (<span style="color: #0000ff">this</span>.cursor != ArrayList.<span style="color: #0000ff">this</span><span style="color: #000000">.size);
} </span><span style="color: #0000ff">public</span><span style="color: #000000"> E next() {
checkForComodification();
</span><span style="color: #008000">/**</span><span style="color: #008000"> 省略此处代码 </span><span style="color: #008000">*/</span><span style="color: #000000">
} </span><span style="color: #0000ff">public</span> <span style="color: #0000ff">void</span><span style="color: #000000"> remove() {
</span><span style="color: #0000ff">if</span> (<span style="color: #0000ff">this</span>.lastRet &lt; 0<span style="color: #000000">)
</span><span style="color: #0000ff">throw</span> <span style="color: #0000ff">new</span><span style="color: #000000"> IllegalStateException();
checkForComodification();
</span><span style="color: #008000">/**</span><span style="color: #008000"> 省略此处代码 </span><span style="color: #008000">*/</span><span style="color: #000000">
} </span><span style="color: #0000ff">final</span> <span style="color: #0000ff">void</span><span style="color: #000000"> checkForComodification() {
</span><span style="color: #0000ff">if</span> (ArrayList.<span style="color: #0000ff">this</span>.modCount == <span style="color: #0000ff">this</span><span style="color: #000000">.expectedModCount)
</span><span style="color: #0000ff">return</span><span style="color: #000000">;
</span><span style="color: #0000ff">throw</span> <span style="color: #0000ff">new</span><span style="color: #000000"> ConcurrentModificationException();
}
}</span></pre>

从上面的源代码我们可以看出,迭代器在调用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);
/** 省略此处代码 */
}
</span><span style="color: #0000ff">private</span> <span style="color: #0000ff">void</span> ensureCapacityInternal(<span style="color: #0000ff">int</span><span style="color: #000000"> paramInt) {
</span><span style="color: #0000ff">if</span> (<span style="color: #0000ff">this</span>.elementData ==<span style="color: #000000"> EMPTY_ELEMENTDATA)
paramInt </span>= Math.max(10<span style="color: #000000">, paramInt);
ensureExplicitCapacity(paramInt);
} </span><span style="color: #0000ff">private</span> <span style="color: #0000ff">void</span> ensureExplicitCapacity(<span style="color: #0000ff">int</span><span style="color: #000000"> paramInt) {
</span><span style="color: #0000ff">this</span>.modCount += 1; <span style="color: #008000">//</span><span style="color: #008000">修改modCount</span>
<span style="color: #008000">/**</span><span style="color: #008000"> 省略此处代码 </span><span style="color: #008000">*/</span><span style="color: #000000">
}

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;

}

</span><span style="color: #0000ff">private</span> <span style="color: #0000ff">void</span> fastRemove(<span style="color: #0000ff">int</span><span style="color: #000000"> paramInt) {
</span><span style="color: #0000ff">this</span>.modCount += 1; <span style="color: #008000">//</span><span style="color: #008000">修改modCount</span>
<span style="color: #008000">/**</span><span style="color: #008000"> 省略此处代码 </span><span style="color: #008000">*/</span><span style="color: #000000">
} </span><span style="color: #0000ff">public</span> <span style="color: #0000ff">void</span><span style="color: #000000"> clear() {
</span><span style="color: #0000ff">this</span>.modCount += 1; <span style="color: #008000">//</span><span style="color: #008000">修改modCount</span>
<span style="color: #008000">/**</span><span style="color: #008000"> 省略此处代码 </span><span style="color: #008000">*/</span><span style="color: #000000">
}</span></pre>

从上面的源代码我们可以看出,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++)];
}
    </span><span style="color: #008000">/**</span><span style="color: #008000"> 省略此处代码 </span><span style="color: #008000">*/</span><span style="color: #000000">
}</span></pre>

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();
}
}
</span><span style="color: #0000ff">final</span> <span style="color: #0000ff">void</span><span style="color: #000000"> setArray(Object[] paramArrayOfObject) {
</span><span style="color: #0000ff">this</span>.array =<span style="color: #000000"> paramArrayOfObject;
}</span></pre>

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

Java提高篇(三四)-----fail-fast机制的更多相关文章

  1. Java提高篇(三三)-----Map总结

    在前面LZ详细介绍了HashMap.HashTable.TreeMap的实现方法,从数据结构.实现原理.源码分析三个方面进行阐述,对这个三个类应该有了比较清晰的了解,下面LZ就Map做一个简单的总结. ...

  2. Java提高篇(三二)-----List总结

    前面LZ已经充分介绍了有关于List接口的大部分知识,如ArrayList.LinkedList.Vector.Stack,通过这几个知识点可以对List接口有了比较深的了解了.只有通过归纳总结的知识 ...

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

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

  4. Java提高篇(二八)------TreeSet

    与HashSet是基于HashMap实现一样,TreeSet同样是基于TreeMap实现的.在<Java提高篇(二七)-----TreeMap>中LZ详细讲解了TreeMap实现机制,如果 ...

  5. java提高篇(十七)-----异常(二)

          承接上篇博文:java提高篇-----异常(一) 五.自定义异常 Java确实给我们提供了非常多的异常,但是异常体系是不可能预见所有的希望加以报告的错误,所以Java允许我们自定义异常来表 ...

  6. java提高篇(十五)-----关键字final

    在程序设计中,我们有时可能希望某些数据是不能够改变的,这个时候final就有用武之地了.final是java的关键字,它所表示的是“这部分是无法修改的”.不想被改变的原因有两个:效率.设计.使用到fi ...

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

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

  8. java提高篇---Iterator

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

  9. Java提高篇(三二)-----List总结

    前面LZ已经充分介绍了有关于List接口的大部分知识,如ArrayList.LinkedList.Vector.Stack,通过这几个知识点能够对List接口有了比較深的了解了.仅仅有通过归纳总结的知 ...

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

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

随机推荐

  1. 给JavaScript初学者的24条最佳实践

    ­.fluid-width-video-wrapper { width: 100%; position: relative; padding: 0 } .fluid-width-video-wrapp ...

  2. MvcPager 免费开源分页控件3.0版发布!

    MvcPager 3.0版在原2.0版的基础上进行了较大的升级,对MvcPager脚本插件重写并进行了大量优化.修复了部分bug并新增了客户端Javascript API等功能,使用更方便,功能更强大 ...

  3. mybatis框架中分页的实现

    2.分页的实现? 分页的时候考虑的问题: 分页的大小,分页的索引. 比如:分页的大小为10,分页的起始索引为1(索引从1开始) 第一页:1到10.    起始行号: (页的索引-1)*分页大小+1 结 ...

  4. 【转载】Adapter用法总结大全

    下面的是看到的比较好的地址: Android各种Adapter的用法:                 http://my.oschina.net/u/658933/blog/372151 Andro ...

  5. gruntJs篇之connect+watch自动刷新

    grunt很强大,可以帮我我们解决很多繁琐的操作,虽然刚接触不久,但依然感受到其强大之处,这篇记录一下通过grunt.js实现事实刷新页面, 省去了编码 -> 保存 -> F5..F5.. ...

  6. 领域模型驱动设计(Domain Driven Design)入门概述

    软件开发要干什么: 反映真实世界要自动化的业务流程 解决现实问题 领域Domain Domain特指软件关注的领域 在不能充分了解业务领域的情况下是不可能做出一个好的软件 领域建模 领域模型驱动设计 ...

  7. [UCSD白板题] Greatest Common Divisor

    Problem Introduction The greatest common divisor \(GCD(a, b)\) of two non-negative integers \(a\) an ...

  8. xilium CefGlue集成包

    最近很苦B的要做一个C#的HTM5项目,build了一下xilium CefGlue包,提供下载地址,供那些无法下载的同学们使用. http://yun.baidu.com/s/1slEdNEt

  9. JAVA/Android Map与String的转换方法

    在Android开发中 Map与String的转换在,在一些需求中经常用到,使用net.sf.json.JSONObject.fromObject可以方便的将string转为Map.但需要导入jar包 ...

  10. UOJ#34 FFT模板题

    写完上一道题才意识到自己没有在博客里丢过FFT的模板-- 这道题就是裸的多项式乘法,可以FFT,可以NTT,也可以用Karasuba(好像有人这么写没有T),也可以各种其他分治乘法乱搞-- 所以我就直 ...