(转)java 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示例
- 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 遍历: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;
- 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
- /** 省略此处代码 */
- }
- ublic 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也是相当有损耗的。
(转)java fail-fast机制的更多相关文章
- Fail Fast and Fail Safe Iterators in Java
https://www.geeksforgeeks.org/fail-fast-fail-safe-iterators-java/ Fail Fast and Fail Safe Iterators ...
- fail fast和fail safe策略
优先考虑出现异常的场景,当程序出现异常的时候,直接抛出异常,随后程序终止 import java.util.ArrayList; import java.util.Collections; impor ...
- 快速失败(fail—fast)和 安全失败(fail—safe)
快速失败(fail-fast) 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的结构进行了修改(增加.删除),则会抛出Concurrent Modification Exception. 原理 ...
- Java 动态代理机制分析及扩展
Java 动态代理机制分析及扩展,第 1 部分 王 忠平, 软件工程师, IBM 何 平, 软件工程师, IBM 简介: 本文通过分析 Java 动态代理的机制和特点,解读动态代理类的源代码,并且模拟 ...
- Java中JIN机制及System.loadLibrary() 的执行过程
Android平台Native开发与JNI机制详解 http://mysuperbaby.iteye.com/blog/915425 个人认为下面这篇转载的文章写的很清晰很不错. 注意Android平 ...
- Java 动态代理机制分析及扩展--转
http://www.ibm.com/developerworks/cn/java/j-lo-proxy1/#icomments http://www.ibm.com/developerworks/c ...
- Java 动态代理机制分析及扩展,第 1 部分
Java 动态代理机制分析及扩展,第 1 部分 http://www.ibm.com/developerworks/cn/java/j-lo-proxy1/ 本文通过分析 Java 动态代理的机制和特 ...
- 深入理解 Java 中 SPI 机制
本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/vpy5DJ-hhn0iOyp747oL5A作者:姜柱 SPI(Service Provider ...
- Java的多线程机制系列:不得不提的volatile及指令重排序(happen-before)
一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ...
- java的锁机制
一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在Java里边就是拿到某个同步对象的锁(一个对象只有一把锁): 如果这个时候同步对象的锁被其他线程拿走了,他(这个线 ...
随机推荐
- 算法笔记_086:蓝桥杯练习 9-2 文本加密(Java)
目录 1 问题描述 2 解决方案 1 问题描述 问题描述 先编写函数EncryptChar,按照下述规则将给定的字符c转化(加密)为新的字符:"A"转化"B" ...
- Spring容器的属性配置详解的六个专题
在spring IOC容器的配置文件applicationContext.xml里,有一些配置细节值得一提.我们将一些问题归结为以下几个专题. 专题一:字面值问题 配置的bean节点中的值,我们提 ...
- Android实现截图分享qq,微信
代码地址如下:http://www.demodashi.com/demo/13292.html 前言 现在很多应用都有截图分享的功能,今天就来讲讲截图分享吧 今天涉及到以下内容: android权限设 ...
- struct2常用标签
Struts2常用标签总结 一 介绍 1.Struts2的作用 Struts2标签库提供了主题.模板支持,极大地简化了视图页面的编写,而且,struts2的主题.模板都提供了很好的扩展性.实现了 ...
- offsetof宏的实现
1.c语言的结构体中,因为字节对齐的问题,导致成员地址并不能根据类型的大小进行计算.例如: struct test { char ch; int a; } printf("test的大小=% ...
- Java小型知识点
1. API 1.1 byte[].File.InputStream 互相转换 1.将File.FileInputStream 转换为byte数组: File file = new File(&quo ...
- atitit.atiOrm.js v2 q61 版本新特性.docx
atitit.atiOrm.js v2 q61 版本新特性.docx 1. V1新特性如下1 1.1. V2规划,直接生成sql在js端1 2. Orm设计框架图1 2.1. atiOrm.js的原理 ...
- VS编译duilib项目时候的错误解决方法整理
@1:找不到Riched20.lib 用everything等软件搜索下磁盘.找到所在的文件夹加入到vs的库文件夹就可以.我得是C:\Program Files (x86)\Microsoft SDK ...
- 1.Vector(向量容器)
一.概述 Vectors 包含着一系列连续存储的元素,其行为和数组类似.访问Vector中的任意元素或从末尾添加元素都可以在常量级时间复杂度内完成,而查找特定值的元素所处的位置或是在Vector中插入 ...
- 分页技术框架(Pager-taglib)学习一(页面分页)
一.Pager-taglib简介 1.Pager-taglib,支持多种风格的分页显示.实际上她是一个Jsp标签库,为在JSP上显示分页信息而设计的一套标签,通过这些标签的不同的组合,会形成多 ...