ArrayList的remove(Object o)方法 與Iterator 的remove()方法有什麼區別

其實我們日常學習的知識點和問題都是一環扣一環的,很多問題都是相關聯的,本次主要圍繞着以下問題展開 :

  1. ArrayList的remove(Object o)方法 與Iterator 的remove()方法有什麼區別?(會引出Fail-Fast機制)
  2. 什麼是Fail-Fast機制與Fail-Safe機制機制?(會引出CopyOnWrite思想)
  3. 什麼是CopyOnWrite思想?
  4. CopyOnWrite的使用場景與優缺點?

其實我們平時也會遇到這樣的需求:將列表中滿足某種條件的數據刪除;稍後讓我們藉助下面這個邏輯來簡單描述下,就是將列表中將等於"1002"的元素刪除,此處就拿遍歷ArrayList集合當作事例,代碼如下:

private static void testUnsafeArrayList() {
        List<String> arr = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            arr.add("100"+i);
        }
        Iterator it = arr.iterator();
        while (it.hasNext()) {
            String value = String.valueOf(it.next());
            if ("1002".equals(value)){
                arr.remove(value); //使用這個會拋異常ConcurrentModificationException
//                it.remove();  //arr.remove(value)替換成 it.remove()沒異常
            }
        }
        System.out.println("刪除後的數據:");
        Iterator tmp = arr.iterator();
        while (tmp.hasNext()) {
            String value = String.valueOf(tmp.next());
            System.out.println(value);
        }
    }

運行結果:
執行代碼使用 arr.remove(value)你會發現報錯如下:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
    at java.util.ArrayList$Itr.next(ArrayList.java:859)
    at com.liujc.demo.TestUtil.testUnsafeArrayList(TestUtil.java:69)
    at com.liujc.demo.TestUtil.main(TestUtil.java:38)

Process finished with exit code 1

替換方案:
然後使用it.remove()替換arr.remove(value)即可正常運行:

這是什麼原因呢?
原因是迭代器在遍歷時直接訪問集合中的內容,並且在遍歷過程中使用一個 modCount 變量。集合在被遍歷期間如果內容發生變化,就會改變 modCount 的值。
每當迭代器使用 hashNext()/next() 遍歷下一個元素之前,都會檢測 modCount 變量是否爲 expectedModCount 值,相等的話就返回遍歷;否則拋出異常,終止遍歷。

這個理由說的似乎有點道理,我要眼見爲實。
接下來讓我們根據源碼來分析下:
首先我們上面代碼中利用了ArrayList的remove方法會拋出異常,其實在remove方法中調用了fastRemove, 在fastRemove中執行modCount++;更新了更改次數,查看源碼:

public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

private void fastRemove(int index) {
        //此處記修改次數
        modCount++; 
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

在依次遍歷迭代器Iterator 時調用其next()方法,通過下面next()源碼中可以看到首先調用checkForComodification()方法,該方法主要判斷 expectedModCount 是否與 modCount 相等,不相等拋出異常:

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];
        }
final void checkForComodification() {
             //此處的modCount已經在remove操作中變更
            if (modCount != expectedModCount) 
                throw new ConcurrentModificationException();
        }

替換方案中使用迭代器的remove方法爲什麼沒有問題?
查看ArrayList的源碼中ListItr繼承了Itr,對應的remove()中執行expectedModCount = modCountexpectedModCount重新賦值繞過後續檢查,源碼如下:

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

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            //此處重新賦值,會繞過後續next()方法中的checkForComodification檢查條件
            expectedModCount = modCount;  
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
      }

其實我們在遇到問題時儘量做到不但要知其然還要知其所以然,要有這種探索精神,但是看源碼有時候也要點到爲止。好了,迴歸正題,上面這個案例分析其實已經描述了Fail-Fast 機制的實現原理,那麼現在對Fail-Fast 機制是否瞭解了呢?

什麼是快速失敗機制(Fail-Fast)安全失敗機制(Fail-Safe)
也許日常沒太注意過這兩種機制,但是我們肯定都碰到過,其實在我們平時接觸的HashMap、ArrayList 這些集合類,這些在 java.util 包的集合類就都是快速失敗的(Fail-Fast);而 java.util.concurrent 包下的類都是安全失敗(Fail-Safe),比如:CopyOnWriteArrayList等。

Fail-Fast機制

  • 什麼是Fail-Fast機制?
    Fail-fast 機制是 Java 集合中的一種錯誤機制。
    在使用迭代器對集合對象進行遍歷的時候,如果 A 線程正在對集合進行遍歷,此時 B 線程對集合進行修改(增加、刪除),或者 A 線程在遍歷過程中對集合進行修改,都會導致 A 線程拋出 ConcurrentModificationException 異常。
  • Fail-fast 機制實現原理
    對於實現原理其實根據上面的案例分析更清楚些,這裏簡單描述下:
    迭代器在遍歷時直接訪問集合中的內容,並且在遍歷過程中使用一個 modCount 變量。集合在被遍歷期間如果內容發生變化,就會改變 modCount 的值。
    每當迭代器使用 hashNext()/next() 遍歷下一個元素之前,都會檢測 modCount 變量是否與 expectedModCount 值相等,相等的話就返回遍歷;否則拋出異常,終止遍歷。其實在構造迭代器 Iterator 時,通過 expectedModCount 記錄了集合當前(開始遍歷元素之前)的修改次數:
    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();
          }
      }
    

Fail-Safe機制

  • 什麼是Fail-Safe機制?
    採用安全失敗機制的集合容器,在遍歷時不是直接在集合內容上訪問的,而是先複製原有集合內容,在拷貝的集合上進行遍歷。
    由於迭代時是對原集合的拷貝進行遍歷,所以在遍歷過程中對原集合所作的修改並不能被迭代器檢測到,故不會拋 ConcurrentModificationException 異常。
  • Fail-Safe機制原理:
    在原集合的 copy 上遍歷,也就是運用CopyOnWrite思想。
  • 缺點:
    • 創建原集合的 copy 需要額外的空間和時間上的開銷;
    • 可能存在髒讀的數據,不能保證遍歷的是最新的內容。

CopyOnWrite思想(寫入時複製)

  • CopyOnWrite思想概念
    是一種計算機程序設計領域的優化策略。其核心思想是,如果有多個調用者同時請求相同資源(如內存或磁盤上的數據存儲),他們會共同獲取相同的指針指向相同的資源,直到某個調用者試圖修改資源的內容時,系統纔會真正複製一份專用副本給該調用者,而其他調用者所見到的最初的資源仍然保持不變。這過程對其他的調用者都是透明無感知的。此作法主要的優點是如果調用者沒有修改該資源,就不會有副本被創建,因此多個調用者只是讀取操作時可以共享同一份資源。
  • CopyOnWrite容器
    CopyOnWrite容器即寫時複製的容器。通俗的理解是當我們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行Copy,複製出一個新的容器,然後新的容器裏添加元素,添加完元素之後,再將原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行併發的讀,而不需要加鎖,因爲當前容器不會添加任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。
  • CopyOnWriteArrayList分析
    • 概念:
      併發版ArrayList,底層結構也是數組,和ArrayList不同之處在於:當新增和刪除元素時會創建一個新的數組,在新的數組中增加或者排除指定對象,最後用新增數組替換原來的數組。
    • 適用場景:
      由於讀操作不加鎖,寫(增、刪、改)操作加鎖,因此適用於讀多寫少的場景。
    • 侷限:
      由於讀的時候不會加鎖(讀的效率高,就和普通ArrayList一樣),讀取的當前副本,因此可能讀取到髒數據。

CopyOnWriteArrayList的整個add操作都是在鎖的保護下進行的。
這樣做是爲了避免在多線程併發add的時候,複製出多個副本出來,可能會出現髒讀的情況,導致最終的數組數據不是我們期望的。

    public boolean add(E e) {
     //1、先加鎖
     final ReentrantLock lock = this.lock;
     lock.lock();
     try {
         Object[] elements = getArray();
         int len = elements.length;
         //2、拷貝數組
         Object[] newElements = Arrays.copyOf(elements, len + 1);
         //3、將元素加入到新數組中
         newElements[len] = e;
         //4、將array引用指向到新數組
         setArray(newElements);
         return true;
     } finally {
        //5、解鎖
         lock.unlock();
     }
    }

線程併發的寫,則通過鎖來控制,如果有線程併發的讀,則分以下幾種情況:
1、如果寫操作未完成,那麼直接讀取原數組的數據;
2、如果寫操作完成,但是引用還未指向新數組,那麼也是讀取原數組數據;
3、如果寫操作完成,並且引用已經指向了新的數組,那麼直接從新數組中讀取數據。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章