Java中的fail-fast和fast-safe机制


1. 引入

不管是在ArrayList、Hashtable还是HashMap的源码中,我们在属性字段都可以看见modCount字段的存在。它用来表示集合修改的次数,例如当执行put操作时就会将modCount加1,都是在其他常用的方法中并没有看到它的具体使用。而它却是fail-fast和fast-safe机制的一种重要参考值,下面就了解一下这两种机制,以及modCount在其中所起到的作用。

并发修改:当一个或多个线程正在遍历一个集合Collection,此时另一个线程修改了这个集合的内容(添加,删除或者修改)。


2. fail-fast

fail-fast(快速失败)机制指的是当遍历集合的过程中,如果集合的结构发生了改变,例如进行了put操作或是扩容操作,那么程序就会抛出Concurrent Modification Exception。java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

通常来说,我们在遍历集合的目的就是为了查看集合中存在哪些元素,并不会做其他的骚操作。例如,我们使用iterator来遍历ArrayList:

@Test
public void testIterator(){
    List<Integer> list = new ArrayList<>();

    Collections.addAll(list, 2,4,5,6,12,38);

    Iterator<Integer> iterator = list.iterator();
    while(iterator.hasNext()){
        System.out.println(iterator.next());
    }
}

执行单元测试,程序的输出为:

2
4
5
6
12
38

但是如果在遍历的过程中往list中又添加了一个元素呢?如下所示:

@Test
public void testIterator(){
    List<Integer> list = new ArrayList<>();

    Collections.addAll(list, 2,4,5,6,12,38);

    Iterator<Integer> iterator = list.iterator();
    while(iterator.hasNext()){
        list.add(100);
        System.out.println(iterator.next());
    }
}

执行单元测试,程序就会抛出ConcurrentModificationException异常。

java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at List.ListDemo.testIterator(ListDemo.java:106)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

这就是fail-fast机制的体现,但是具体在源码中是如何体现的呢?既然它和遍历操作有关,那么我们就需查看相关的源码。ArrayList中有关迭代器的源码如下:

// iterator方法本质上就是返回了一个Itr对象
public Iterator<E> iterator() {
    return new Itr();
}

private class Itr implements Iterator<E> {
    // 游标,用于遍历的过程中取值
    int cursor;   
    // index of last element returned; -1 if no such
    int lastRet = -1; 
    // 这里终于看到了modCount,遍历操作前设置expectedModCount为此时的modCount
    int expectedModCount = modCount;
	
    // 无参构造
    Itr() {}
	
    // 判断是否还有元素可供遍历
    public boolean hasNext() {
        // 即判断游标是不是走到了list的最后一个位置
        return cursor != size;
    }
	
    // 获取下一个元素
    @SuppressWarnings("unchecked")
    public E next() {
        // 首先就需要检查expectedModCount和modCount是否相等
        // 如果不等,说明list结构发生了改变,直接抛ConcurrentModificationException
        checkForComodification();
        // 根据游标获取值
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        // 如果游标值超过了elementData的长度,说明结构发生了改变,直接抛ConcurrentModificationException
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        // 否则返回游标指向的元素,并更新lastRet的值
        return (E) elementData[lastRet = i];
    }
	
    // 使用迭代器移除元素
    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        // 检查
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            // 移除后修改expectedModCount为最新的modCount
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            // 只有发生了越界异常才会抛ConcurrentModificationException
            throw new ConcurrentModificationException();
        }
    }


    final void checkForComodification() {
        // 判断iterator中的expectedModCount和当前集合中的modCount值是否相等
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

3. fail-safe

fail-safe(安全失败)机制是指任何对集合结构的修改都会在一个复制的集合上进行修改,因此不会抛出ConcurrentModificationException。java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

但是fail-safe机制有两个问题:

  • 需要复制集合,产生大量的无效对象,开销大
  • 无法保证读取的数据是目前原始数据结构中的数据,因为复制操作执行后,集合可能会发生改变
@Test
public void test() {
    Map<String, Integer> map = new ConcurrentHashMap<>();

    map.put("Forlogen", 10);
    map.put("Kobe", 24);
    map.put("James", 23);

    Iterator<String> iterator = map.keySet().iterator();

    while (iterator.hasNext())
    {
        System.out.println(map.get(iterator.next()));
        map.put("Yao", 11);
    }
}

fail-safe的具体体现可见java.util.concurrentx中类的实现,例如使用 CopyOnWriterArrayList代替ArrayList,CopyOnWriterArrayList在是使用上跟ArrayList几乎一样,CopyOnWriter是写时复制的容器(COW),在读写时是线程安全的。该容器在对add和remove等操作时,并不是在原数组上进行修改,而是将原数组拷贝一份,在新数组上进行修改,待完成后,才将指向旧数组的引用指向新数组,所以对于CopyOnWriterArrayList在迭代过程并不会发生fail-fast现象。但 CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值