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是线程安全的,所以我们在多线程的环境下面需要去使用这个就可以了。

 

posted @ 2022-03-28 10:46  r1-12king  阅读(230)  评论(0编辑  收藏  举报