一、快速失败(fail-fast)
1、什么是快速失败(fail-fast)?
2、案例
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)呢?
2、
三、Arrays.asList()避坑指南
1、简介
1 String[] myArray = { "Apple", "Banana", "Orange" };
2 List<String> myList = Arrays.asList(myArray);
3
4 //上⾯两个语句等价于下⾯⼀条语句
5 List<String> myList = Arrays.asList("Apple","Banana", "Orange");
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 开发⼿册》对其的描述
3、使⽤时的注意事项总结
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
Integer[] myArray = { 1, 2, 3 };
1 List myList = Arrays.asList(1, 2, 3);
2 myList.add(4);//运⾏时报错:UnsupportedOperationException
3 myList.remove(1);//运⾏时报错:UnsupportedOperationException
4 myList.clear();//运⾏时报错:UnsupportedOperationException
1 List myList = Arrays.asList(1, 2, 3);
2 System.out.println(myList.getClass());//class 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 }
1 public E remove(int index) {
2 throw new UnsupportedOperationException();
3 }
参考:https://javadevnotes.com/java-array-to-list-examples