什么是 fail-fast
https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html
fail-fast 机制是Java集合(Collection)中的一种错误机制。
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的结构进行了修改(增加、删除),则会抛出Concurrent Modification Exception 【并发修改异常】。
举个例子:
在多线程环境下,线程1正在对集合进行遍历,此时线程2对集合进行修改 , 很容易抛出Concurrent Modification Exception 。
当然了,在单线程的情况下,遍历时对集合进行修改也会抛出Concurrent Modification Exception
此类的返回的迭代器iterator和 listIterator方法是快速失败的:如果列表在任何时间后,迭代器创建结构修饰,以任何方式除非通过迭代器自身 remove或 add方法,迭代器都将抛出 Concurrent Modification Exception。
因此,面对并发修改,迭代器快速而干净地失败,而不是冒着在未来不确定的时间出现任意、非确定性行为的风险。
源码解读
Itr
在遍历的时候对集合修改会发生fail-fast,遍历集合------> 迭代器
/** * An optimized version of AbstractList.Itr */ private class Itr implements Iterator<E> { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; Itr() {} public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { checkForComodification(); 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]; } 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(); } } @Override @SuppressWarnings("unchecked") public void forEachRemaining(Consumer<? super E> consumer) { Objects.requireNonNull(consumer); final int size = ArrayList.this.size; int i = cursor; if (i >= size) { return; } final Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } while (i != size && modCount == expectedModCount) { consumer.accept((E) elementData[i++]); } // update once at end of iteration to reduce heap write traffic cursor = i; lastRet = i - 1; checkForComodification(); } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
看到了吧, checkForComodification
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
在modCount != expectedModCount的时候抛出了ConcurrentModificationException,
而在next方法中上来就是调用checkForComodification,所以遍历集合才会可能抛出并发修改异常。
那接下来就研究 modCount 和 expectedModCount 什么时候会不相等就行了呗。
在创建一个迭代器后,expectedModCount的初始值就是modCount了,
- 对集合修改会改变
modCount
,
expectedModCount
只会在迭代器的remove
方法中被修改为modCount
这都是
中的内容,除了modCount 。 modCount 是ArrayList的常量,默认值 为0
为什么对集合的结构进行修改会发生并发修改异常-源码分析
那我们说,在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的结构进行了修改(增加、删除),则会抛出Concurrent Modification Exception 【并发修改异常】。
修改方法之 remove
modCount++ , 后面modCount会和expectedModCount不相等,进而抛出并发修改异常。
修改方法之 add
ensureCapacityInternal方法里对modCount++操作, 改变了modCount的值,所以调用
那set方法会触发 fast fail吗?
答案是不会。
set没有对modCount++,所以对集合的某个元素进行修改并不会fail-fast
案例分享
【案例一】
List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); list.add("4"); Iterator<String> iter = list.iterator(); while (iter.hasNext()) { String tmp = iter.next(); System.out.println(tmp); if (tmp.equals("1")) { list.remove("1"); } }
1 Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911) at java.util.ArrayList$Itr.next(ArrayList.java:861) at com.artisan.fastfail.FastFailTest.main(FastFailTest.java:25)
调用了 list# remove方法
【案例二】
List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); list.add("4"); Iterator<String> iter = list.iterator(); while (iter.hasNext()) { String tmp = iter.next(); System.out.println(tmp); if (tmp.equals("3")) { list.remove("3"); } }
1 2 3
调用了 list# remove方法 , 居然没有抛出并发修改异常????
remove倒数第二个元素,然而这时就没有抛出异常了 。 再分析分析吧
cursor是下一个要返回的变量的下标
lastRet是上一个返回过的变量的下标
hasNext方法告诉我们只有在下一个变量的下标不等于size的时候会告诉我们集合还有下一个元素。
但是在remove的时候,size- -了,那么删除“3”这个元素后,size变为3,而此时cursor也是3,那么再走到hasNext时,就发现cursor和size相等了,那么就会退出遍历,“4”压根就不会被遍历到。
所以没有抛出异常,因为remove后就退出了,还没来得及走到next方法呢~
【案例三】
List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); list.add("4"); Iterator<String> iter = list.iterator(); while (iter.hasNext()) { String tmp = iter.next(); System.out.println(tmp); if (tmp.equals("4")) { list.remove("4"); } }
1 2 3 4 Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911) at java.util.ArrayList$Itr.next(ArrayList.java:861) at com.artisan.fastfail.FastFailTest.main(FastFailTest.java:25)
接上个案例
那 删除“4”,也就是最后一个元素,按理说删了最后一个元素不就退出了吗?走不到下一次的next方法呀?
其实是不对的,删完“4”并没有就直接退出 ! remove后size变成了3,但此时cursor是4,那么走到hasNext时,发现4!=3,就会再次进入循环,那么结果…走到了next方法,抛出了异常。。。
【案例四】
List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); list.add("4"); for (String i : list) { if ("1".equals(i)) { list.remove("1"); } }
Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911) at java.util.ArrayList$Itr.next(ArrayList.java:861) at com.artisan.fastfail.FastFailTest.main(FastFailTest.java:22)
用增强for循环遍历的,反编译class , 和用迭代器实质是一样的 。
【案例五】
List<String> list = Arrays.asList("1", "2", "3", "4"); for (String i : list) { if ("1".equals(i)) { list.remove("1"); } }
Exception in thread "main" java.lang.UnsupportedOperationException at java.util.AbstractList.remove(AbstractList.java:161) at java.util.AbstractList$Itr.remove(AbstractList.java:374) at java.util.AbstractCollection.remove(AbstractCollection.java:293) at com.artisan.fastfail.FastFailTest.main(FastFailTest.java:21)
用了Array.asList()方法生成的集合,抛出的是UnsupportedOperationException,发现asList生成的ArrayList是个静态内部类,并非java.util.ArrayList, 并没有这些方法。
所以不能对asList生成的ArrayList进行增删改
Java开发规范01 - 集合篇_Arrays.asList 坑
【案例六】
List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); list.add("4"); Iterator<String> iter = list.iterator(); while (iter.hasNext()) { String tmp = iter.next(); System.out.println(tmp); if (tmp.equals("1")) { iter.remove(); } }
1 2 3 4
【案例七】
```java // Java code to illustrate // Fail Fast Iterator in Java import java.util.HashMap; import java.util.Iterator; import java.util.Map; public class FailFastExample { public static void main(String[] args) { Map<String, String> cityCode = new HashMap<String, String>(); cityCode.put("Delhi", "India"); cityCode.put("Moscow", "Russia"); cityCode.put("New York", "USA"); Iterator iterator = cityCode.keySet().iterator(); while (iterator.hasNext()) { System.out.println(cityCode.get(iterator.next())); // adding an element to Map // exception will be thrown on next call // of next() method. cityCode.put("Istanbul", "Turkey"); } } }
India Exception in thread "main" java.util.ConcurrentModificationException at java.util.HashMap$HashIterator.nextNode(HashMap.java:1442) at java.util.HashMap$KeyIterator.next(HashMap.java:1466) at FailFastExample.main(FailFastExample.java:18)
// Java code to demonstrate remove // case in Fail-fast iterators import java.util.ArrayList; import java.util.Iterator; public class FailFastExample { public static void main(String[] args) { ArrayList<Integer> al = new ArrayList<>(); al.add(1); al.add(2); al.add(3); al.add(4); al.add(5); Iterator<Integer> itr = al.iterator(); while (itr.hasNext()) { if (itr.next() == 2) { // will not throw Exception itr.remove(); } } System.out.println(al); itr = al.iterator(); while (itr.hasNext()) { if (itr.next() == 3) { // will throw Exception on // next call of next() method al.remove(3); } } } }
[1, 3, 4, 5] Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList$Itr.next(ArrayList.java:851) at FailFastExample.main(FailFastExample.java:28)
阿里巴巴Java开发手册中的规定
如何避免fail-fast抛异常
- 如果非要在遍历的时候修改集合,那么建议用迭代器的remove等方法,而不是用集合的remove等方法
- 并发的环境,那还要对Iterator对象加锁, 也可以直接使用Collections.synchronizedList
- CopyOnWriteArrayList(采用fail-safe)