java中的fail-fast(快速失敗)機制


java中的fail-fast(快速失敗)機制

簡介

  fail-fast機制,即快速失敗機制,是java集合中的一種錯誤檢測機制。當在迭代集合的過程中對該集合的結構改變是,就有可能會發生fail-fast,即跑出ConcurrentModificationException異常。fail-fast機制並不保證在不同步的修改下一定拋出異常,它只是近最大努力去拋出,所以這種機制一般僅用於檢測bug

 

fail-fast的出現場景

在我們常見的java集合中就可能出現fail-fast機制,比如常見的ArrayList,HashMap.在多線程和單線程環境下都有可能出現快速失敗

1.單線程環境下的fail-fast例子:
    public static void main(String[] args) {
           List<String> list = new ArrayList<>();
           for (int i = 0 ; i < 10 ; i++ ) {
                list.add(i + "");
           }
           Iterator<String> iterator = list.iterator();
           int i = 0 ;
           while(iterator.hasNext()) {
                if (i == 3) {
                     list.remove(3);
                }
                System.out.println(iterator.next());
                i ++;
           }
     }

控制台打印:

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 com.example.springboot_demo.fail_fast_safe.ArrayListFailFast.main(ArrayListFailFast.java:24)

該段代碼定義了一個Arraylist集合,並使用迭代器遍歷,在遍歷過程中,刻意在某一步迭代中remove一個元素,這個時候,就會發生fail-fast

2.多線程環境下
public class ArrayListFailFastThreadPool {
    public static List<String> list = new ArrayList<>();
​
    private static class MyThread1 extends Thread {
        @Override
        public void run() {
            Iterator<String> iterator = list.iterator();
            while(iterator.hasNext()) {
                String s = iterator.next();
                System.out.println(this.getName() + ":" + s);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            super.run();
        }
    }
​
    private static class MyThread2 extends Thread {
        int i = 0;
        @Override
        public void run() {
            while (i < 10) {
                System.out.println("thread2:" + i);
                if (i == 2) {
                    list.remove(i);
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                i ++;
            }
        }
    }
​
    public static void main(String[] args) {
        for(int i = 0 ; i < 10;i++){
            list.add(i+"");
        }
        MyThread1 thread1 = new MyThread1();
        MyThread2 thread2 = new MyThread2();
        thread1.setName("thread1");
        thread2.setName("thread2");
        thread1.start();
        thread2.start();
    }
​
}

 

控制台打印:

Exception in thread "thread1" java.util.ConcurrentModificationException
thread2:3
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at com.example.springboot_demo.fail_fast_safe.ArrayListFailFastThreadPool$MyThread1.run(ArrayListFailFastThreadPool.java:20)

 

啟動兩個線程,分別對其中一個對list進行迭代,另一個在線程1的迭代過程中去remove一個元素,結果也是拋出了java.util.ConcurrentModificationException

Fail-fast原理

fail-fast是如何拋出ConcurrentModificationException異常的,又是在什么情況下才會拋出?

我們知道,對於集合入list,map類,我們都是可以通過迭代器來遍歷,而Iterator其實只是一個接口,具體的實現還是具體的集合類的內部類實現Iterator並實現相關方法。這里我們就以ArrayList類為例。在ArrayList中,當調用list.iterator()時,其源碼是:

 public Iterator<E> iterator() {
        return new 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;
​
        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();
        }
    }
​
     可以看出,該方法才是判斷是否拋出ConcurrentModificationException異常的關鍵。在該段代碼中,當modCount != expectedModCount時,就會拋出該異常。很明顯expectedModCount在整個迭代過程除了一開始賦予初始值modCount外,並沒有再發生改變,所以可能發生改變的就只有modCount.
在前面關於ArrayList擴容機制的分析中,可以知道在ArrayList進行add,remove,clear等涉及到修改集合中的元素個數的操作時,modCount就會發生改變(modCount++)所以當另一個線程(並發修改)或者同一個線程遍歷過程中,調用相關方法使集合的個數發生改變,就會使modCount發生變化,這樣在checkForComodification方法中就會拋出ConcurrentModificationException異常。
類似的,hashMap中發生的原理也是一樣的。
避免fail-fast

方法1

在單線程的遍歷過程中,如果要進行remove操作,可以調用迭代器的remove方法而不是集合類的remove方法

 public static void main(String[] args) {
           List<String> list = new ArrayList<>();
           for (int i = 0 ; i < 10 ; i++ ) {
                list.add(i + "");
           }
           Iterator<String> iterator = list.iterator();
           int i = 0 ;
           while(iterator.hasNext()) {
                if (i == 3) {
                     iterator.remove(); //迭代器的remove()方法
                }
                System.out.println(iterator.next());
                i ++;
           }
     }

 

方法2

  使用java並發包(java.util.concurrent)中的類來代替ArrayList 和hashMap。

        CopyOnWriterArrayList代替ArrayList,CopyOnWriterArrayList在是使用上跟ArrayList幾乎一樣,CopyOnWriter是寫時復制的容器(COW),在讀寫時是線程安全的。該容器在對add和remove等操作時,並不是在原數組上進行修改,而是將原數組拷貝一份,在新數組上進行修改,待完成后,才將指向舊數組的引用指向新數組,所以對於CopyOnWriterArrayList在迭代過程並不會發生fail-fast現象。但 CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。

        對於HashMap,可以使用ConcurrentHashMap,ConcurrentHashMap采用了鎖機制,是線程安全的。在迭代方面,ConcurrentHashMap使用了一種不同的迭代方式。在這種迭代方式中,當iterator被創建后集合再發生改變就不再是拋出ConcurrentModificationException,取而代之的是在改變時new新的數據從而不影響原有的數據 ,iterator完成后再將頭指針替換為新的數據 ,這樣iterator線程可以使用原來老的數據,而寫線程也可以並發的完成改變。即迭代不會發生fail-fast,但不保證獲取的是最新的數據。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM