zoukankan      html  css  js  c++  java
  • Java SE之快速失败(Fast-Fail)与快速安全(Fast-Safe)的区别[集合与多线程/增强For](彻底详解)

    声明

      特点:基于JDK源码进行分析。

      研究费时费力,如需转载或摘要,请显著处注明出处,以尊重劳动研究成果:博客园 - https://www.cnblogs.com/johnnyzen/p/10547179.html,侵权必究,蟹蟹理解。

    /**
     * @IDE: Created by IntelliJ IDEA.
     * @Author: 千千寰宇
     * @Email: 1125418540@qq.com
     * @Date: 2019/3/17  12:46:34
     * @Description: Java SE之Fast-Fail与Fast-Safe的区别[集合与多线程/增强For]
     */
    

    [1] Fast-Fail事件

    特征

      ① java.util包下面的所有的集合类

             + ArrayList / HashMap / LinkedList / Iterator / Collections / …

      ②会抛出ConcurrentModificationException异常,一种错误检测机制

      ③产生条件:其一,当多个线程对Collection下的集合类进行操作时,若其中某一个线程通过iterator去遍历集合时,该集合的内容被其他线程所改变;其二,单线程环境下,如果单次遍历对该包下的集合对象进行多次写操作,则也可能抛出ConcurrentModificationException异常。

      ④解决办法:通过util.concurrent包下的相应类去处理,则:不会产生fast-fail事件,即fast-safe

    案例分析

      【测试源码】

    public static void arrayList(){
        List list = new ArrayList();
        list.add(1);
        list.add(2);
        list.add(3);
        for(Object obj : list){
            System.out.println(obj);
            list.remove(obj); //使用集合对象list删除元素
        }
    }
    【运行结果】

    【原因】

    //ArrayList的内部数组
    transient Object[] elementData; 
    //ArrayList的iterator()方法
    public Iterator<E> iterator() {
        return new Itr();
    }
    //ArrayList的remove()方法
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index); //关键调用处:修改 modCount++
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);//关键调用处:修改 modCount++
                    return true;
                }
        }
        return false;
    }
    //ArrayList的fastRemove()方法
    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
    //对list对象内部数组的元素进行删除,但会修改modCount值
        elementData[--size] = null; // clear to let GC do its work
    }
    
    //ArrayList 的私有内部类Itr implements Iterator的next()
    private class Itr implements Iterator<E> {
      int expectedModCount = modCount;
    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();//检查modCount与expectedModCount
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
    public void remove() {
         if (lastRet < 0)
             throw new IllegalStateException();
         checkForComodification();
         try {
             ArrayList.this.remove(lastRet);
    //对list对象内部数组的元素进行删除,但不会修改modCount值
             cursor = lastRet;
             lastRet = -1;
             expectedModCount = modCount;//即 使用ArrayList对象获取的iterator对象的remove方法删除元素时,将设置expectedModCount = modCount ,以确保下一次能通过checkForComodification()
         } catch (IndexOutOfBoundsException ex) {
             throw new ConcurrentModificationException();
         }
      }//Itr.remove() end
    }//Itr end
    1.Java.util包下的集合类(如:ArrayList)的迭代器(如:私有内部类Itr)在遍历时直接访问集合中的内容,并且在遍历过程中使用了modCount变量
    2.Java.util包下的集合类(如:ArrayList)的集合对象中在被遍历期间如果内容发生变化,就会改变modCount的值(如:ArrayList的remove())
    3.每当集合类(如:ArrayList)对象的迭代器使用 hashNext()/next()遍历下一个元素之前,都会检测modCount变量和expectedmodCount值是否相等
    4.如果相等就返回遍历结果,否则抛出异常,终止遍历
    (整个分析过程,可参见上述ArrayList中源码)引用于[2]

    【另外:关于ArrayList.remove()与其ArrayList.iterator().reomve()的区别?】
    上述案例的源码中使用 Collection(ArrayList 的父类) 集合对象list中的 remove()方法。该方法只能从集合中删除元素,不能把迭代器iterator中的元素也删除了。即:
    ArrayList.remove()
    删除元素时,修改modCount++
    ArrayList.Itr.remove()
    删除元素时,调用ArrayList.remove()后,立即设置modCount=expectedmodCount,
    避免了并发修改异常(ConcurrentModificationException)。
     
    值得注意的是:两方法均是对同一对象的同一动态数组(elementData)进行操作。那么,在多线程环境下,如果多个线程对同一list对象进行操作时,elementData便是实际上的共享互斥资源,则 可能造成数据污染的非线程安全事件。
     
    【产生问题】
    1.【单线程环境】当使用foreach循环时,欲实现对util包下的集合对象一边遍历一边增、删集合元素时,该怎么办?
    答:其一,每次遍历进行单次写操作,通过集合对象的迭代器iterator对应增、删元素(Itr.remove())的方法,线程安全。
    //demo 不会抛并发修改异常
    Iterator it = list.iterator();
    while(it.hasNext()){
        Object obj = it.next();
        System.out.println(obj);
        if(obj.equals(1)){
            it.remove();//使用迭代器的remove()方法
        }
    } 
      其二,每次遍历进行多次写操作,不一定线程安全。原因是:并发修改异常触发时,必须满足checkForComodification()方法的modCount == expectedmodCount的成立。那么,如果集合发生变化时修改modCount值, 刚好有设置为了expectedmodCount值, 则异常不会抛出。
      比如:先删除一条数据,再添加一条数据。
    //此时,不会抛并发修改异常
    List list = new ArrayList();
    list.add(1);
    list.add(2);
    list.add(3);
    for(Object obj : list){
        System.out.println(obj);
        if(obj == new Integer(2)){ //使用equals()也会抛并发修改异常
            list.remove(obj); 
            list.add(2);
        }
    }
    

       2.【多线程环境】当多个线程同时对util包下的同一集合对象进行写操作时,是否线程安全?

      答:不能依赖于ConcurrentModificationException异常是否抛出,而进行并发操作的编程, 该异常只检测并发修改的BUG。

    【结论】

      java.util包下的集合类都是快速失败(fail-fast)机制的, 不能在多线程下发生并发修改,在单线程下,也不推荐迭代过程中修改该集合类对象的元素值。引用于[2]

    [2] Fast-Safe事件

      ①java.util.concurrent包下面的所有的类

             + BlockingDeque / LinkedBlockingDeque / SynchronousQueue

             + ConcurrentHashMap / ConcurrentLinkedQueue / CopyOnWriteArrayList

             + …

           ②不会抛出并发修改异常;采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

           ③结论:java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。 引用于[2]

      ④原理:由于迭代时是对原集合的拷贝的值进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会出发ConcurrentModificationExceptio

      ⑤缺陷:基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地, 迭代器并不能访问到修改后的内容 (简单来说就是, 迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的) 引用于[2]

    [3] 补充:增强for

    特征

      ①增强型for | For/in | Foreach循环语法:for(ElementType element:arrayName){}

      ②不支持遍历时修改。详见上述fast-fail中关于“每次遍历单/多次修改”。

      ③对遍历的集合需要做null判断,不然可能引发空指针异常

      ④增强for循环的内部:

        遍历数组,foreach 循环实际上还是用的普通的 for 循环 引用[1]

        遍历集合,foreach 循环实际上是用的 iterator 迭代器迭代 引用[1]

      备注:始于JDK1.5

    案例分析

    【测试源码】

    //实验
    public class Foreach {
        public static void foreach(){
            int [] array = {1,2,3};
            for(int i : array){
                System.out.println(i);
            }
            List list = new ArrayList();
            list.add(1);
            list.add(2);
            list.add(3);
            for(Object obj : list){
                System.out.println(obj);
            }
        }
    }
    

    【反编译结果】javap -c sourceFile

     

    [4] 参考文献

      [1] Java中的增强 for 循环/ foreach

         备注:该大V博文关于fast-fail与fast-safe的探究,认为是增强for循环基于多线程是错误的,毫无依据,JDK源码中也并未体现。

      [2]【荐】fail-fast和fail-safe的介绍和区别

      [3] fail-fast和fail-safe详解

      [4]【荐】Concurrent下的线程安全集合

  • 相关阅读:
    ES6-01 2018-02-06
    8.1 IO类
    2.4 const限定符
    C++命名空间
    win7系统docker安装ubuntu
    win7安装docker
    wuzhicms 查看模板中的所有可用变量和值
    wuzhicms上传弹出层,如何返回数据到当前页面?
    wuzhicms 无规律推荐位标签的嵌套使用
    【wuzhicms】apache 设置禁止访问某些文件或目录
  • 原文地址:https://www.cnblogs.com/johnnyzen/p/10547179.html
Copyright © 2011-2022 走看看