Java学习 | Java集合的fail-fast机制

1. fail-fast简介

fail-fast是Java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就有可能会发生fail-fast事件。
例如:当前有一个线程A在对一个集合(如HashMapArrayList)进行遍历从操作,在这个过程中,有另外一个线程B对该集合的结构进行了修改,而这就可能会抛出ConcurrentModificationException异常,产生fail-fast事件。

2. fail-fast示例

View Code
import java.util.*;
import java.util.concurrent.*;

/*
 * 功能: java集合中fast-fail的测试程序。
 *
 * fast-fail事件产生的条件:当多个线程对Collection进行操作时,若其中某一个线程通过iterator去遍历集合时,该集合的内容被其他线程所改变;则会抛出ConcurrentModificationException异常。
 *  (1)使用ArrayList时,会产生fast-fail事件,抛出ConcurrentModificationException异常
 *  (2)使用时CopyOnWriteArrayList,不会产生fast-fail事件
 */
public class FastFailTest {

    // 两个线程同时对 list 集合进行操作
    private static List<String> list = new ArrayList<String>();
    public static void main(String[] args) {
    
        // 同时启动两个线程对list进行操作!
        new ThreadOne().start();
        new ThreadTwo().start();
    }

    private static void printAll() {
        System.out.println("");

        String value = null;
        Iterator iter = list.iterator();
        while(iter.hasNext()) {
            value = (String)iter.next();
            System.out.print(value+", ");
        }
    }

    /**
     * 向list中依次添加0,1,2,3,4,5,每添加一个数之后,就通过printAll()遍历整个list
     */
    private static class ThreadOne extends Thread {
        public void run() {
            int i = 0;
            while (i<6) {
                list.add(String.valueOf(i));
                printAll();
                i++;
            }
        }
    }

    /**
     * 向list中依次添加10,11,12,13,14,15,每添加一个数之后,就通过printAll()遍历整个list
     */
    private static class ThreadTwo extends Thread {
        public void run() {
            int i = 10;
            while (i<16) {
                list.add(String.valueOf(i));
                printAll();
                i++;
            }
        }
    }
}

运行结果:程序抛出java.util.ConcurrentModificationException异常。
结果分析

  • 主程序同时启动两个线程对list进行操作
  • 线程A向list中添加0~5,每添加一个数,则打印整个list; 线程Blist中添加10~15,每添加一个数,则同样打印整个list;
  • 当其中一个线程在遍历打印list的同时,另外一个线程向list中添加了一个数(既改变了集合的结构),就是就会抛出java.util.ConcurrentModificationException异常。

3. fail-fast解决办法

fail-fast机制,是一种错误检测机制,JDK并不保证fail-fast机制一定会发生
若在多线程环境下使用fail-fast机制的集合,建议使用java.util.concurrent包下的类去取代java.util包下的类。
例如在上面例子中就可以将ArrayList替换成CopyOnWriteArrayList:
将:

private static List<String> list = new ArrayList<String>();

替换成:

private static List<String> list = new CopyOnWriteArrayList<String>();

CopyOnWriteArrayList所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。
该类产生的开销比较大,但是在两种情况下,它非常适合使用。

  • 在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时。
  • 当遍历操作的数量大大超过可变操作的数量时。

4. fail-fast原理

产生fail-fast事件,是通过抛出ConcurrentModificationException异常来触发的,而要了解fail-fast机制,我们就需要了解ArrayList是如何抛出ConcurrentModificationException异常的。
从前面的代码中,我们了解到ArrayList是在遍历的过程中,抛出该异常,既在操作Iterator时抛出。

  1. ArrayList 的 Iterator 是在父类 AbstractList.java 中实现的,通过源码发现,Iteratornext()remove()方法的开始,都会执行一个checkForComodification()方法。
  2. 如果checkForComodification()方法中,modCount 不等于 expectedModCount,则抛出ConcurrentModificationException异常。
  3. 现在我们知道,要了解什么时候会抛出异常,就需要了解,在什么时候modCountexpectedModCount变得不相等了。
  4. 在创建 Iterator对象时,modCount 被赋值给 expectedModCount,所以 modCount 不等于 expectedModCount只可能是modCount被修改导致两个变量的不等,所以现在问题变为,modCount在何时被修改。
  5. 通过ArrayList的源码我们发现,在add()remove()clear()方法中,只要涉及到修改集合中的元素个数的时候,都会改变modCount的值。

4.1 fail-fast的产生步骤

通过以上的步骤找到fail-fast产生的原因,下面就来看看fail-fast到底是如何发生的:

  • 新建了一个ArrayList对象 -> list,并向其中添加若干个元素;
  • 新建一个线程A,通过Iteratorlist进行循环遍历
  • 新建一个线程B,其从list中删除一个节点X
  • 就是就可能会发生我们所说的fail-fast事件了:
    • 某一个时刻,线程A创建了Iterator对象,此时节点X任然在list中,在创建Iterator对象时,modCount == expectedModCount(假设它们的值为N
    • 线程Alist进行遍历的某一个时刻,线程B执行了,从list中删除了节点X。具体到源码中就是,线程B执行了remove()方法,在其中执行了modCount++,导致 modCount变为了N+1
    • 之后线程A继续遍历的时候,执行next()方法,调用我们上面说到的checkForComodification()比较modCountexpectedModCount的值,此时modCount == N+1expectedModCount == N,两者不等,就抛出ConcurrentModificationException异常,即产生fail-fast事件。

5. 解决fail-fast的原理

  • 和 ArrayList 继承于 AbstractList 不同,CopyOnWriteArrayList没有继承于AbstractList,它仅仅只是实现了 List 接口。
  • ArrayList的 iterator() 函数返回的Iterator是在AbstractList中实现的;而 CopyOnWriteArrayList 是自己实现 Iterator
  • 同时,新建COWIterator时,它会将集合中的元素保存到一个新的拷贝数组中。这样,当原始集合的数据改变,拷贝数据中的值也不会变化。
  • ArrayList 的 Iterator 实现类中调用 next() 时,会调用 checkForComodification() 比较expectedModCountmodCount 的大小;但是,CopyOnWriteArrayList 的 Iterator 实现类中,没有所谓的checkForComodification(),更不会抛出 ConcurrentModificationException 异常!






参考博文1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值