Java提高篇(三四)-----fail-fast机制

在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 遍历:0ThreadTwo run:0ThreadTwo run:1ThreadTwo run:2ThreadTwo run:3ThreadTwo run:4ThreadTwo run:5Exception 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        /** 省略此处代码 */    }       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;    }    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也是相当有损耗的。

参考文档: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. 【npm】利用npm安装/删除/发布/更新/撤销发布包

      什么是npm? npm是javascript的包管理工具,是前端模块化下的一个标志性产物 简单地地说,就是通过npm下载模块,复用已有的代码,提高工作效率   1.从社区的角度:把针对某一特定问题 ...

  2. 【javascript】原生js更改css样式的两种方式

    下面我给大家介绍的是原生js更改CSS样式的两种方式: 1通过在javascript代码中的node.style.cssText="css表达式1:css表达式2:css表达式3  &quo ...

  3. IO模型 IO多路复用

    阻塞IO 用socket 一定会用到accept recv recvfrom这些方法正常情况下 accept recv recvfrom都是阻塞的 非阻塞IO 如果setblocking(False) ...

  4. 防止活动上线时 微信openid 被伪造的解决办法

    背景 前不久上线了一个 campaign 项目,一个 h5,后端为php,用户可以在微信中通过网页授权的方式登录,然后用微信 openid 作为唯一标识符进行签到和抽奖的操作. 结果后期出现了很多脏数 ...

  5. git log 高级用法

    转自:https://github.com/geeeeeeeeek/git-recipes/wiki/5.3-Git-log%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95 内 ...

  6. MySQL 并行复制(MTS) 从库更新的记录不存在实际却存在

    目录 背景 版本 分析 测试 背景 开了并行复制的半同步从库SQL 线程报1032错误,异步复制从库没有报错,偶尔会出现这种 版本 mysql 5.7.16 redhat 6.8 mysql> ...

  7. SpringBoot配置文件application.properties详解

    喜欢的朋友可以关注下,粉丝也缺. 相信很多的码友在接触springboot时,不知道怎么去配置一些项目中需要的配置,比如数据源,tomcat调优,端口等等,下面我就给大家奉献出一些项目中常用的配置信息 ...

  8. Golang 中哪些值是不可以寻址的

    不可以寻址, 指的是不能通过&获得其地址. golang中不能寻址的可以总结为:不可变的,临时结果和不安全的.只要符合其中任何一个条件,它就是不可以寻址的. 具体为: 常量的值. 基本类型值的 ...

  9. 《Python编程从入门到实践》--- 学习过程笔记(2)变量和简单数据类型

    一.变量无需声明 二.变量命名规则 (1)变量名只能包括字母.数字和下划线: (2)变量名不能包含空格: (3)变量名不能使用Python关键字和函数名(保留字不可以做变量名); (4)简短易懂,清晰 ...

  10. POJ 2871

    #include<iostream> #include<stdio.h> #include<iomanip> using namespace std; int ma ...