• Java 集合:(三十二)集合中其他重要问题


    一、快速失败(fail-fast)

      1、什么是快速失败(fail-fast)?

        快速失败(fail-fast) 是 Java 集合(Collection)的⼀种错误检测机制。
        在使⽤迭代器对集合进⾏遍历的时候,我们在多线程下操作⾮安全失败(fail-safe)的集合类可能就会触发 fail-fast 机制,导致抛出 ConcurrentModificationException 异常。
     
        另外,在单线程下,如果在遍历过程中对集合对象的内容进⾏了修改的话也会触发 fail-fast 机制
        :增强 for 循环也是借助迭代器进⾏遍历。

      2、案例

        例如:多线程下,如果线程 1 正在对集合进⾏遍历,此时线程 2 对集合进⾏修改(增加、删除、修改),或者线程 1 在遍历过程中对集合进⾏修改,都会导致线程 1 抛出 ConcurrentModificationException 异常。
        例如:当某一个线程 A 通过 iterator 去遍历某集合的过程中,若该集合的内容被其他线程所改变 了,那么线程 A 访问集合时,就会抛出 ConcurrentModificationException 异常,产生 fail-fast 事 件。这里的操作主要是指 add、remove 和 clear,对集合元素个数进行修改。

      3、为什么这样呢?

        这就是常说的fail-fast(快速失败)机制,这个就需要从一个变量说起

    transient int modCount;
    

          在HashMap等集合中有一个名为modCount的变量,它用来表示集合被修改的次数,修改指的是插入元素或删除元素,可以回去看看插入删除部分的源码,在最后都会对modCount进行自增。

        当我们在遍历HashMap时,每次遍历下一个元素前都会对modCount进行判断,若和原来的不一致说明集合结果被修改过了,然后就会抛出异常,这是Java集合的一个特性,我们这里以keySet为例,看看部分相关源码:

     1 public Set<K> keySet() {
     2     Set<K> ks = keySet;
     3     if (ks == null) {
     4         ks = new KeySet();
     5         keySet = ks;
     6     }
     7     return ks;
     8 }
     9 
    10 final class KeySet extends AbstractSet<K> {
    11     public final Iterator<K> iterator()     { return new KeyIterator(); }
    12     // 省略部分代码
    13 }
    14 
    15 final class KeyIterator extends HashIterator implements Iterator<K> {
    16     public final K next() { return nextNode().key; }
    17 }
    18 
    19 /*HashMap迭代器基类,子类有KeyIterator、ValueIterator等*/
    20 abstract class HashIterator {
    21     Node<K,V> next;        //下一个节点
    22     Node<K,V> current;     //当前节点
    23     int expectedModCount;  //修改次数
    24     int index;             //当前索引
    25     //无参构造
    26     HashIterator() {
    27         expectedModCount = modCount;
    28         Node<K,V>[] t = table;
    29         current = next = null;
    30         index = 0;
    31         //找到第一个不为空的桶的索引
    32         if (t != null && size > 0) {
    33             do {} while (index < t.length && (next = t[index++]) == null);
    34         }
    35     }
    36     //是否有下一个节点
    37     public final boolean hasNext() {
    38         return next != null;
    39     }
    40     //返回下一个节点
    41     final Node<K,V> nextNode() {
    42         Node<K,V>[] t;
    43         Node<K,V> e = next;
    44         if (modCount != expectedModCount)
    45             throw new ConcurrentModificationException();//fail-fast
    46         if (e == null)
    47             throw new NoSuchElementException();
    48         //当前的链表遍历完了就开始遍历下一个链表
    49         if ((next = (current = e).next) == null && (t = table) != null) {
    50             do {} while (index < t.length && (next = t[index++]) == null);
    51         }
    52         return e;
    53     }
    54     //删除元素
    55     public final void remove() {
    56         Node<K,V> p = current;
    57         if (p == null)
    58             throw new IllegalStateException();
    59         if (modCount != expectedModCount)
    60             throw new ConcurrentModificationException();
    61         current = null;
    62         K key = p.key;
    63         removeNode(hash(key), key, null, false, false);//调用外部的removeNode
    64         expectedModCount = modCount;
    65     }
    66 }

        相关代码如下,可以看到若modCount被修改了则会抛出ConcurrentModificationException异常。相关代码如下,可以看到若modCount被修改了则会抛出ConcurrentModificationException异常。

    if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
    

      

      4、ArrayList 类中

        (1)添加方法

        (2)删除方法

        可以看出,两个操作都会进行 modCount 的修改。

        当我们使用迭代器或 foreach 遍历时,如果你在 foreach 遍历时,自动调用迭代器的迭代方法,此时在遍历过程中调用了集合的add,remove方法时,modCount就会改变,而迭代器记录的modCount是开始迭代之前的,如果两个不一致,就会报异常,说明有两个线路(线程)同时操作集合。这种操作有风险,为了保证结果的正确性, 避免这样的情况发生,一旦发现modCount与expectedModCount不一致,立即报错。

        此类的 iterator 和 listIterator 方法返回的迭代器是快速失败的:在创建迭代器之后,除非通过迭代器自身的 remove 或 add 方法从结构上对列表进行修改, 否则在任何时间以任何方式对列表进行修改, 迭代器都会抛出 ConcurrentModificationException。 因此,面对并发的修改,迭代器很快就会完全失败, 而不是冒着在将来某个不确定时间发生任意不确定行为的风险。

      5、那么如何在遍历时删除元素呢?

        我们可以看看迭代器自带的remove方法,其中最后两行代码如下:

    removeNode(hash(key), key, null, false, false);//调用外部的removeNode
    expectedModCount = modCount;
    

        意思就是会调用外部remove方法删除元素后,把 modCount 赋值给 expectedModCount,这样的话两者一致就不会抛出异常了,所以我们应该这样写:

    1         Map<String, Integer> map = new HashMap<>();
    2         map.put("1", 1);
    3         map.put("2", 2);
    4         map.put("3", 3);
    5         Iterator<String> iterator = map.keySet().iterator();
    6         while (iterator.hasNext()){
    7             if (iterator.next().equals("2"))
    8                 iterator.remove();
    9         }    

     

        这里还有一个知识点就是在遍历HashMap时,我们会发现  遍历的顺序和插入的顺序不一致,这是为什么?

        在HashIterator源码里面可以看出,它是先从桶数组中找到包含链表节点引用的桶。然后对这个桶指向的链表进行遍历。遍历完成后,再继续寻找下一个包含链表节点引用的桶,找到继续遍历。找不到,则结束遍历。这就解释了为什么遍历和插入的顺序不一致,不懂的同学请看下图:

           

      7、解决快速失败办法

        建议使用“java.util.concurrent 包下的类”去取代“java.util 包下的类”。可以这么理解:在遍历之前,把 modCount 记下来 expectModCount,后面 expectModCount 去 和 modCount 进行比较,如果不相等了,证明已并发了,被修改了,于是抛出 ConcurrentModificationException 异常。

    二、安全失败(fail-safe)

      1、什么是安全失败(fail-safe)呢?

        采⽤安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,⽽是先复制原有集合内容,在
    拷⻉的集合上进⾏遍历。所以,在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛
    ConcurrentModificationException 异常。

      2、

     

    三、Arrays.asList()避坑指南

      1、简介

        Arrays.asList() 在平时开发中还是⽐较常⻅的,我们可以使⽤它将⼀个数组转换为⼀个 List 集合。
    1 String[] myArray = { "Apple", "Banana", "Orange" };
    2 List<String> myList = Arrays.asList(myArray);
    3 
    4 //上⾯两个语句等价于下⾯⼀条语句
    5 List<String> myList = Arrays.asList("Apple","Banana", "Orange");
     
        JDK 源码对于这个⽅法的说明:
    1 /**
    2 *返回由指定数组⽀持的固定⼤⼩的列表。此⽅法作为基于数组和基于集合的API
    3 之间的桥梁,与 Collection.toArray()结合使⽤。返回的List是可序
    4 列化并实现RandomAccess接⼝。
    5 */
    6 public static <T> List<T> asList(T... a) {
    7     return new ArrayList<>(a);
    8 }

      2、《阿⾥巴巴 Java 开发⼿册》对其的描述

        Arrays.asList() 将数组转换为集合后,底层其实还是数组,《阿⾥巴巴 Java 开发⼿册》对于这个⽅法有如下描述:

          

      3、使⽤时的注意事项总结

        (1)传递的数组必须是对象数组,⽽不是基本类型。
          Arrays.asList() 是泛型⽅法,传⼊的对象必须是对象数组。
    1   int[] myArray = { 1, 2, 3 };
    2   List myList = Arrays.asList(myArray);
    3   System.out.println(myList.size());//1
    4   System.out.println(myList.get(0));//数组地址值
    5   System.out.println(myList.get(1));//报错:ArrayIndexOutOfBoundsException
    6   int [] array=(int[]) myList.get(0);
    7   System.out.println(array[0]);//1
        当传⼊⼀个原⽣数据类型数组时, Arrays.asList() 的真正得到的参数就不是数组中的元素,⽽是数组对象本身!
        此时 List 的唯⼀元素就是这个数组,这也就解释了上⾯的代码。
        我们使⽤包装类型数组就可以解决这个问题。
    Integer[] myArray = { 1, 2, 3 };
    

      

        (2)使⽤集合的修改⽅法: add() 、 remove() 、 clear() 会抛出异常。
    1 List myList = Arrays.asList(1, 2, 3);
    2 myList.add(4);//运⾏时报错:UnsupportedOperationException
    3 myList.remove(1);//运⾏时报错:UnsupportedOperationException
    4 myList.clear();//运⾏时报错:UnsupportedOperationException
        Arrays.asList() ⽅法返回的并不是 java.util.ArrayList ,⽽是 java.util.Arrays 的⼀个内部类,这个内部类并没有实现集合的修改⽅法或者说并没有重写这些⽅法。
    1 List myList = Arrays.asList(1, 2, 3);
    2 System.out.println(myList.getClass());//class java.util.Arrays$ArrayList
        下面是 java.util.Arrays$ArrayList 的简易源码,我们可以看到这个类重写的⽅法有哪些。
     1  private static class ArrayList<E> extends AbstractList<E>
     2  implements RandomAccess, java.io.Serializable
     3  {
     4    ...
     5    @Override
     6    public E get(int index) {
     7      ...
     8    }
     9    @Override
    10    public E set(int index, E element) {
    11      ...
    12    }
    13    @Override
    14    public int indexOf(Object o) {
    15      ...
    16    }
    17    @Override
    18    public boolean contains(Object o) {
    19      ...
    20    }
    21    @Override
    22    public void forEach(Consumer<? super E> action) {
    23      ...
    24    }
    25    @Override
    26    public void replaceAll(UnaryOperator<E> operator) {
    27      ...
    28    }
    29    @Override
    30    public void sort(Comparator<? super E> c) {
    31      ...
    32    }
    33  }
      我们再看⼀下 java.util.AbstractList 的 remove() ⽅法,这样我们就明⽩为啥会抛出 UnsupportedOperationException
    1 public E remove(int index) {
    2     throw new UnsupportedOperationException();
    3 }

      参考:https://javadevnotes.com/java-array-to-list-examples

    四、

  • 相关阅读:
    不等高cell的搭建(一)
    重复点击主界面(TabBar)按钮刷新界面--点击状态栏回到顶部
    如何学习新框架(保存图片到相册)
    上下拉刷新
    MVVM框架思想
    不等高cell的tableView界面搭建
    UITabBarController底层实现
    封装业务类
    RSS阅读器
    构造队列
  • 原文地址:https://www.cnblogs.com/niujifei/p/14798908.html
Copyright © 2020-2023  润新知