java中的fast-fail机制
概念
fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
分析
先看一个代码:
1 public class Test { 2 private static List<Integer> list = new ArrayList<>(); 3 public static void main(String[] args) { 4 Thread t1 = new ThreadOne(); 5 Thread t2 = new ThreadTwo(); 6 t1.start(); 7 t2.start(); 8 } 9 10 static void print(){ 11 Iterator<Integer> iter = list.iterator(); 12 while(iter.hasNext()){ 13 System.out.println(iter.next()); 14 } 15 } 16 17 static class ThreadOne extends Thread{ 18 @Override 19 public void run(){ 20 for(int i=0; i<3; i++){ 21 list.add(i); 22 print(); 23 } 24 } 25 } 26 27 static class ThreadTwo extends Thread{ 28 @Override 29 public void run(){ 30 for(int i=0; i<3; i++){ 31 list.add(i); 32 print(); 33 } 34 } 35 } 36 }
执行结果如下:
可以看到, 出现了ConcurrentModificationException异常,这就是fast-fail机制。
我们都已经知道了,fail-fast快速失败是在迭代的时候产生的,但是是如何产生的呢?下面我们再来深入的分析一下:
根本原因:
从前面我们知道fail-fast是在操作迭代器时产生的。现在我们来看看ArrayList中迭代器的源代码:
1 private class Itr implements Iterator<E> { 2 int cursor; // index of next element to return 3 int lastRet = -1; // index of last element returned; -1 if no such 4 int expectedModCount = modCount; 5 6 Itr() {} 7 8 public boolean hasNext() { 9 return cursor != size; 10 } 11 12 @SuppressWarnings("unchecked") 13 public E next() { 14 checkForComodification(); 15 int i = cursor; 16 if (i >= size) 17 throw new NoSuchElementException(); 18 Object[] elementData = ArrayList.this.elementData; 19 if (i >= elementData.length) 20 throw new ConcurrentModificationException(); 21 cursor = i + 1; 22 return (E) elementData[lastRet = i]; 23 } 24 25 public void remove() { 26 if (lastRet < 0) 27 throw new IllegalStateException(); 28 checkForComodification(); 29 30 try { 31 ArrayList.this.remove(lastRet); 32 cursor = lastRet; 33 lastRet = -1; 34 expectedModCount = modCount; 35 } catch (IndexOutOfBoundsException ex) { 36 throw new ConcurrentModificationException(); 37 } 38 }
1 final void checkForComodification() { 2 if (modCount != expectedModCount) 3 throw new ConcurrentModificationException(); 4 }
从上面的源代码我们可以看出,迭代器在调用next()、remove()方法时都是调用checkForComodification()方法,它检测modCount == expectedModCount ? 若不等则抛出ConcurrentModificationException 异常,从而产生fail-fast机制。
到了这一步我们也知道了,想要弄清楚fail-fast机制,首先我们需要搞清楚modCount 和expectedModCount。
expectedModCount 是在IteratorTest中定义的:
1 int expectedModCount = modCount;
所以他的值是不可能会修改的,所以会变的就是modCount。
modCount是在 AbstractList 中定义的,为全局变量:
1 protected transient int modCount = 0;
那么他什么时候因为什么原因而发生改变呢?请看ArrayList的源码:
1 public E remove(int index) { 2 rangeCheck(index); 3 4 modCount++; 5 E oldValue = elementData(index); 6 7 int numMoved = size - index - 1; 8 if (numMoved > 0) 9 System.arraycopy(elementData, index+1, elementData, index, 10 numMoved); 11 elementData[--size] = null; // clear to let GC do its work 12 13 return oldValue; 14 } 15 16 17 public boolean add(E e) { 18 ensureCapacityInternal(size + 1); // Increments modCount!! 19 elementData[size++] = e; 20 return true; 21 }v 22 23 24 private void ensureCapacityInternal(int minCapacity) { 25 ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); 26 } 27 28 private void ensureExplicitCapacity(int minCapacity) { 29 modCount++; 30 31 // overflow-conscious code 32 if (minCapacity - elementData.length > 0) 33 grow(minCapacity); 34 }
对于ArrayLis来说,只要是涉及了改变ArrayList元素的个数的方法都会导致modCount的改变。所以我们这里可以判断由于expectedModCount 与modCount的改变不同步,导致两者之间不等,从而产生fail-fast机制。
单线程下面的fast-fail机制
事实上,即使是单线程下也有可能会出现这种情况。
不要在 foreach 循环里进行元素的
remove/add
操作。remove 元素请使用Iterator
方式,如果并发操作,需要对Iterator
对象加锁。
通过反编译你会发现 foreach 语法糖底层其实还是依赖 Iterator
。不过, remove/add
操作直接调用的是集合自己的方法,而不是 Iterator
的 remove/add
方法
这就导致 Iterator
莫名其妙地发现自己有元素被 remove/add
,然后,它就会抛出一个 ConcurrentModificationException
来提示用户发生了并发修改异常。这就是单线程状态下产生的 fail-fast 机制。
1 public class Test { 2 private static List<Integer> list = new ArrayList<>(); 3 public static void main(String[] args) { 4 test(); 5 } 6 7 private static void test(){ 8 List<Integer> list = new ArrayList<>(); 9 list.add(1); 10 list.add(1); 11 list.add(1); 12 list.add(1); 13 for(Integer i: list){ 14 if(i == 1){ 15 list.remove((Object)i); 16 } 17 } 18 } 19 }
解决方案
我们如何去规避这种情况呢?这里有两种解决方案:
- 方案一:在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList(不推荐)
- 方案二:使用CopyOnWriteArrayList来替换ArrayList。
CopyOnWriteArrayList为什么能解决这个问题呢?CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。CopyOnWriteArrayList中add/remove等写方法是需要加锁的,目的是为了避免Copy出N个副本出来,导致并发写。但是。CopyOnWriteArrayList中的读方法是没有加锁的。
我们只需要记住一句话,那就是CopyOnWriteArrayList是线程安全的,所以我们在多线程的环境下面需要去使用这个就可以了。