zoukankan      html  css  js  c++  java
  • Java集合框架中的快速失败(fail—fast)机制

      fail-fast机制,即快速失败机制,是java集合框架中的一种错误检测机制。多线程下用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除),则会抛出Concurrent Modification Exception。fail-fast机制并不保证在不同步的修改下一定会抛出异常,这种机制一般仅用于检测bug。

    那么在实际测试代码当中是如何表现的呢?


    先说结论:在用for遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除),则会抛出ConcurrentModificationException。在单线程下用迭代器遍历修改,则不会报错。在多线程环境下则会报错。

      原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
      这里异常的抛出条件是检测到 modCount!=expectedmodCount这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。

    我们来检测单线程下用增强for循环来更新数据会不会报错:

    import java.util.*;
    public static void main(String[] args) {
    	List<Integer> list = new ArrayList<>();
    	for(int i=0;i<10;i++) {
    	    list.add(i);
    	}
    	for(int n:list){
    	    if(n==6){
    	        list.set(n, 16);
    	    }
    	}
    	System.out.println(list.toString());
    }
    

    输出是[0, 1, 2, 3, 4, 5, 16, 7, 8, 9]

    那如果在遍历中删除参数呢?

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for(int i=0;i<10;i++) {
            list.add(i);
        }
        for(int n:list){
            if(n==6){
                list.remove(n);
            }
        }
        System.out.println(list.toString());
    }
    
    ->Exception in thread "main" java.util.ConcurrentModificationException
    

    这是为什么呢?让我们来看看set的源码:

    public E set(int index, E element) {
       rangeCheck(index);
    
       E oldValue = elementData(index);
       elementData[index] = element;
       return oldValue;
    }
    

    我们发现,进行set操作的时候,modCount并没有自增,所以不会报错。

    那如果用迭代器进行遍历remove呢?

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for(int i=0;i<10;i++) {
            list.add(i);
        }
        Iterator<Integer> it = list.iterator();
        while (it.hasNext()) {
            if (it.next() == 6) {
                it.remove();
            }
        }
        System.out.println(list.toString());
    }
    
    ->[0, 1, 2, 3, 4, 5, 7, 8, 9]
    

    这是因为:

    • 迭代器是作为当前集合的内部类实现的,当迭代器创建的时候保持了当前集合的引用;
    • 集合内部维护一个int变量modCount,用来记录集合被修改的次数,比如add,remove等都会使该字段递增;
    • modCount这个参数记录了某个List改变大小的次数,如果modCount改变的不符合预期,那么就会抛出异常。
    • 迭代器内部也维护着当前集合的修改次数的字段,迭代器创建时该字段初始化为集合的modCount值
    • 当每一次迭代时,迭代器会比较迭代器维护的字段和modCount的值是否相等,如果不相等就抛ConcurrentModifiedException异常;
    • 当然,如果用迭代器调用remove方法,那么集合和迭代器维护的修改次数都会递增,以保持两个状态的一致。

    如下面ArrayList继承的AbstractList源码所示:

    • 定义了modCount
    protected transient int modCount = 0;
    

    ArrayList源码:

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();
    
        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
    public E remove(int index) {
        rangeCheck(index);
    
        modCount++;
        E oldValue = elementData(index);
    
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, 
            elementData, index,numMoved);
        elementData[--size] = null; // clear to let GC do its work
    
        return oldValue;
    }
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    

    可以发现进行remove操作时变量modCount自增了

      Ps:java.util包下的集合类都是采用快速失败机制的,不能在多线程下发生并发修改(迭代过程中被修改)。

    安全失败(fail—safe)


      采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

      原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
      缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
      Ps:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

  • 相关阅读:
    《ASP.NET Core跨平台开发从入门到实战》Web API自定义格式化protobuf
    .NET Core中文分词组件jieba.NET Core
    .NET Core 2.0及.NET Standard 2.0
    Visual Studio 2017 通过SSH 调试Linux 上.NET Core
    Visual Studio 2017 ASP.NET Core开发
    Visual Studio 2017正式版离线安装及介绍
    在.NET Core 上运行的 WordPress
    IT人员如何开好站立会议
    puppeteer(二)操作实例——新Web自动化工具更轻巧更简单
    puppeteer(一)环境搭建——新Web自动化工具(同selenium)
  • 原文地址:https://www.cnblogs.com/keeya/p/9210832.html
Copyright © 2011-2022 走看看