fail-fast機制解讀

fail-fast機制解讀

集合的增刪

​ 平時經常會有一些對集合的增刪場景,尤其是在循環內進行刪除,下面我們看下這幾種場景。

普通for循環

​ 首先,使用 普通for循環可以對集合進行增刪,但增刪後由於普通for循環時是通過下標索引訪問,因此有可能遇到某些數據讀不到的問題。進行完全遍歷時,由於集合長度已發生變化,會拋出IndexOutOfBoundsException下標越界異常。

​ 看一個例子。

            for (int i = 0; i <6 ; i++) {
                System.out.println("讀取"+list.get(i));
                if (3 == i) {
                    list.remove(i);
                }
            }

上述代碼輸出了

讀取0
讀取1
讀取2
讀取3
讀取5
Exception in thread "Thread-1" java.lang.IndexOutOfBoundsException: Index: 5, Size: 5

讀取了3之後直接跳到5,沒有讀取4,因爲刪除掉[3]之後,原本在[4]位置的4下標變爲[3],但循環已經跳過,所以漏了一個。此處可以通過手動控制在刪除後i-1可以避免。但最後的下標越界異常是無法避免的,因此不要在for循環內進行超過1個的集合增刪操作。

增強for循環

​ 在《阿里巴巴JAVA開發規範》中有這樣一段話

【強制】不要在 foreach 循環裏進行元素的 remove/add 操作。remove 元素請使用 Iterator

方式,如果併發操作,需要對 Iterator 對象加鎖。

寫代碼嘗試了一下,在foreach中進行任意的增刪操作,均會拋出ConcurrentModificationException異常。我們寫一段代碼。

						for (int j: list) {
                System.out.println("讀取"+j);
                if (3 == j) {
                    list.remove(j);
                }
            }

然後看下由class文件反編譯後的源碼。

						Iterator var0 = list.iterator();

            while(var0.hasNext()) {
                int j = (Integer)var0.next();
                System.out.println("讀取" + j);
                if (3 == j) {
                    list.remove(j);
                }
            }

​ 可以看到,foreach只是java語言的語法糖,本質上還是由Iterator來迭代的,但在刪除時,調用的是list的remove方法,正是這裏引發了修改異常。

​ 之前在分析list源碼的時候,提到過兩個關鍵變量modCount和expectedModCount,在Itr中進行增刪時,都會進行判斷。

				final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

​ 跑個題,簡單再說一下這兩個變量的作用,首先modCount是AbstractList父類的一個屬性,在集合進行結構變化時都會進行自增以記錄修改次數。

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

​ 在AbstractList中有一個私有內部成員類Itr,通過Iterator接口返回,在使用迭代器對集合迭代時候便會返回這個對象,Itr裏面有一個屬性,exceptedModCount,在Itr進行初始化時候,expectedModCount = modCount。

private class Itr implements Iterator<E> {
        
        int expectedModCount = modCount;  // 賦值,等於modCount
        
        ..........
}
  • modCount記錄的是集合真正的修改次數。

  • expectedModCount記錄的是使用當前迭代器時集合的修改次數。

    看到這裏,大家應該就清楚了,每次開始迭代時候,用exceptedModCount記錄下當前的modCount,這樣,如果集合在其他地方進行了修改,兩個值就會不一樣,直接拋出異常。

那麼它是怎麼保證在當前迭代器進行修改不會有問題的呢?

迭代器遍歷

我們再使用迭代器進行遍歷.

            Iterator var0 = list.iterator();

            while(var0.hasNext()) {
                int j = (Integer)var0.next();
                System.out.println("讀取" + j);
                if (3 == j) {
                    var0.remove();
                }
            }
讀取0
讀取1
讀取2
讀取3
讀取4
讀取5

這段代碼和上面使用foreach的反編譯源碼相比,只有一個改動,那就是將list.remove(),改爲了Iterator.remove,從上面的代碼分析中我們知道了這是調用了Itr對象的remove方法,那這個方法怎麼保證不會拋出異常呢?

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount; // modCount此時已經修改,進行同步
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

可以看到,在Itr中刪除時,remove後會對exceptedModCount和modCount進行同步,從而保證了在當前迭代器內修改不會拋出異常。

引申一點,使用Iterator爲什麼沒有出現使用普通for循環時的下標越界異常呢?因爲Iterator遍歷時候是通過指針操作,在增刪時候會修改指針,避免了這個問題。

fail-fast機制

​ 通過分析上面知道了幾種集合增刪方式,可以看到,在多線程併發讀取和修改集合時,也許並不會真正出問題,但爲了防止這種情況導致的數據不一致性,通過記錄集合的修改次數直接在這種情況出現時拋出異常,實現了fail-fast機制,確保同一時間只有一個線程修改或遍歷線程。

​ 它只是一種錯誤檢測機制,做到了提前檢測,但不一定會發生。

​ 用線程的說法也不準確,因爲通過上面的源碼分析我們知道,fail-fast機制核心是保證了只在當前迭代器內修改,所以在單線程環境下,如果在迭代器外發生了修改(像上面的foreach),也會拋出異常。

知識點

  • 單線程環境和多線程環境都會發生ConcurrentModificationException異常,關鍵看是不是一個迭代器。
  • 使用普通for循環可以少量刪除,但可能會發生數據遺漏和數組下標越界異常。
  • 增強for循環只是迭代器的語法糖,由於觸發了Iterator的fail-fast機制,所以完全無法進行集合修改。

我們應該怎麼做

  1. 在單線程環境下,使用Iterator進行集合的遍歷修改。
  2. 在多線程環境下,使用concurrent中的類來替換ArrayList和HashMap。不推薦使用同步鎖,會額外造成阻塞。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章