那些年應該相識的執行緒安全集合們

  • 2019 年 10 月 6 日
  • 筆記

上篇推文介紹了List的三種實現其實都不是執行緒安全的,文章結尾也回答了如何創建執行緒安全的List,答案是:Collections.synchronizedList

接下來小強再後台收到熱心童鞋(在此特別感謝並艾特三老師)回復了其他方式List執行緒安全的實現方式:CopyOnWriteArrayList

本文就為大家總結下Concurrent下常用的執行緒安全集合們。主要包含以下兩種類型的集合:

– CopyOnWriteArrayList

– BlockingQueue/BlockingDeque

CopyOnWriteArryList

在使用CopyOnWriteArrayList之前,我們先閱讀其源碼了解下它是如何實現的:

    public void add(int index, E element) {         final ReentrantLock lock = this.lock;         lock.lock();          try {             Object[] elements = getArray();             int len = elements.length;              if (index > len || index < 0)                  throw new IndexOutOfBoundsException("Index: "+index+", Size: "+len);             Object[] newElements;             int numMoved = len - index;              if (numMoved == 0)                  newElements = Arrays.copyOf(elements, len + 1);              else {                  newElements = new Object[len + 1];                 System.arraycopy(elements, 0, newElements, 0, index);                 System.arraycopy(elements, index, newElements, index + 1,                                  numMoved);             }              newElements[index] = element;             setArray(newElements);         } finally {             lock.unlock();         }     }

傳統的List在多執行緒同時讀寫的時候會拋出java.util.ConcurrentModificationException,而CopyOnWriteArrayList是使用CopyOnWrite(寫時複製)技術解決了這個問題,上面程式碼是向CopyOnWriteArrayList中add方法的實現(向CopyOnWriteArrayList里添加元素),可以發現在添加的時候是需要加鎖的,否則多執行緒寫的時候會Copy出N個副本出來。這一般需要很大的開銷,但是當遍歷操作的數量大大超過可變操作的數量時,這種方法可能比其他替代方法更有效。

讀的時候不需要加鎖,如果讀的時候有多個執行緒正在向CopyOnWriteArrayList添加數據,讀還是會讀到舊的數據,因為寫的時候不會鎖住舊的CopyOnWriteArrayList

    public E get(int index) {          return get(getArray(), index);     }

所以,通過上面的介紹,CopyOnWriteArrayList並發場景更適合於讀多寫少的場景。

BlokingQueue/BlockingDeque

阻塞隊列BlockingQueue是一個介面,BlockingDeque繼承了BlockingQueue,程式碼如下:

public interface BlockingQueue<E> extends Queue<E> {  }
public interface BlockingDeque<E> extends BlockingQueue<E>, Deque<E> {  }

這裡兩個比較相似的阻塞隊列,BlockingQueue和BlockingDeque,兩個都是隊列,只不過前者只能一端出一端入,後者則可以兩端同時出入,並且他們的實現類都是結構改變執行緒安全的隊列。

BlockingQueue介面的常用實現有以下幾種:

  • ArrayBlockingQueue
  • DelayQueue
  • LinkedBlockingQueue
  • PriorityBlockingQueue

BlockingDeque介面的常用實現有以下幾種:

  • LinkedBlockingDeque

其實兩個隊列從實現思想上比較容易理解,有以下特點:

  • 鏈表結構(動態數組)
  • 通過ReentrantLock實現鎖
  • 利用Condition實現隊列的阻塞等待,喚醒

下面以ArrayBlockingQueue的構造方法舉例來了解以上特點:

    public ArrayBlockingQueue(int capacity, boolean fair) {          if (capacity <= 0)              throw new IllegalArgumentException();          this.items = new Object[capacity];         lock = new ReentrantLock(fair);         notEmpty = lock.newCondition();         notFull =  lock.newCondition();     }

結語

其實還有一種很常見的執行緒安全結合ConcurrentHashMap。它是一種支援高並發、高吞吐量的執行緒安全HashMap實現,其中,在java8中對ConcurrentHashMap的結構進行了很大的改造。

以後的文章,小強可以詳細介紹這部分內容,因為這部分屬於JAVA面試中非常常見的問題,需要專門的篇幅來介紹。在此不贅述。