一、快速失败(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

 

四、

 

posted on 2021-05-22 15:08  格物致知_Tony  阅读(64)  评论(0编辑  收藏  举报