1. 程式人生 > 實用技巧 >[併發程式設計] -- 容器和框架篇

[併發程式設計] -- 容器和框架篇

  • ConcurrentHashMap的實現原理與使用

    • ConcurrentHashMap是執行緒安全且高效的HashMap
    • 為什麼要使用ConcurrentHashMap
      • jdk1.7的HashMap可能導致程式死迴圈:多執行緒會導致HashMapEntry連結串列形成環形資料結構。而jdk1.8引入紅黑樹的資料結構和擴容的優化。

      優化內容具體可參照https://tech.meituan.com/2016/06/24/java-hashmap.html

      • 使用執行緒安全的HashTable效率又非常低下:使用synchronized來保證執行緒安全,但線上程競爭激烈
        的情況下HashTable
        的效率非常低下。(jdk1.7,建議棄用)
      • ConcurrentHashMap的鎖分段技術可有效提升併發訪問率:首先將資料分成一段一段地儲存,然後給每一段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他執行緒訪問。

  • ConcurrentHashMap的結構

  • ConcurrentHashMap是由Segment陣列結構和HashEntry陣列結構組成。
    • Segment是一種可重入鎖(ReentrantLock),在ConcurrentHashMap裡扮演鎖的角色。
    • HashEntry則用於儲存鍵值對資料。
  • ConcurrentHashMap的類圖
  • 結構圖
    public V get(Object key) {
        int hash = hash(key.hashCode());
        return segmentFor(hash).get(key, hash);
    }
  • get操作的高效之處在於整個get過程不需要加鎖,除非讀到的值是空才會加鎖重讀。

    • get方法裡將要使用的共享變數都定義成volatile型別,能夠線上程之間保持可見性,能夠被多執行緒同時讀,並且保證不會讀到過期的值,但是隻能被單執行緒寫(有一種情況可以被多執行緒寫,就是寫入的值不依賴於原值),在get操作裡只需要讀不需要寫共享變數count和value,所以可以不用加鎖;
    • 之所以不會讀到過期的值,是因為根據Java記憶體模型的happen before原則,對volatile欄位的寫入操作先於讀操作,即使兩個執行緒同時修改和獲取volatile變數,get操作也能拿到最新的值。
  • 由於put方法裡需要對共享變數進行寫入操作,所以為了執行緒安全,在操作共享變數時必須加鎖。

    • 判斷是否需要對Segment裡的HashEntry陣列進行擴容
    • 定位新增元素的位置,然後將其放在HashEntry數組裡。
  • size操作,先嚐試2次通過不鎖住Segment的方式來統計各個Segment大小,如果統計的過程中,容器的count發生了變化,則再採用加鎖的方式來統計所有Segment的大小。

    • 使用modCount變數,在put、remove和clean方法裡操作元素前都會將變數modCount進行加1,那麼在統計size前後比較modCount是否發生變化,從而得知容器的大小是否發生變化。
  • ConcurrentLinkedQueue基於連結節點的無界執行緒安全佇列。

    • 採用先進先出的規則對節點進行排序。
    • 當我們獲取一個元素時,它會返回佇列頭部的元素。
    • 採用了“wait-free”演算法(即CAS演算法)來實現,該演算法在Michael&Scott演算法上進行了一些修改。
  • ConcurrentLinkedQueue類圖
  • Java中的阻塞佇列
  • 阻塞佇列(BlockingQueue)是一個支援兩個附加操作的佇列。這兩個附加的操作支援阻塞的插入和移除方法
    • 支援阻塞的插入方法:意思是當佇列滿時,佇列會阻塞插入元素的執行緒,直到佇列不滿。
    • 支援阻塞的移除方法:意思是在佇列為空時,獲取元素的執行緒會等待佇列變為非空

阻塞佇列使用場景:生產者跟消費者。

  • 在阻塞佇列不可用時,這兩個附加操作提供了4種處理方式
    • 丟擲異常:當佇列滿時,如果再往佇列裡插入元素,會丟擲IllegalStateException("Queue full")異常。當佇列空時,從佇列裡獲取元素會丟擲NoSuchElementException異常。
    • 返回特殊值:當往佇列插入元素時,會返回元素是否插入成功,成功返回true。如果是移除方法,則是從佇列裡取出一個元素,如果沒有則返回null。一直阻塞:當阻塞佇列滿時,如果生產者執行緒往佇列裡put元素,佇列會一直阻塞生產者執行緒,直到佇列可用或者響應中斷退出。當佇列空時,如果消費者執行緒從佇列裡take元素,佇列會阻塞住消費者執行緒,直到佇列不為空。
    • 超時退出:當阻塞佇列滿時,如果生產者執行緒往佇列裡插入元素,佇列會阻塞生產者執行緒一段時間,如果超過了指定的時間,生產者執行緒就會退出。
  • jdk 7 提供了7個阻塞佇列
    • 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();
          }
      
    • LinkedBlockingQueue:一個由連結串列結構組成的有界阻塞佇列。
    • PriorityBlockingQueue:一個支援優先順序排序的無界阻塞佇列。
    • DelayQueue:一個使用優先順序佇列實現的無界阻塞佇列。使用場景:
      • 快取系統的設計:可以用DelayQueue儲存快取元素的有效期,使用一個執行緒迴圈查詢DelayQueue,一旦能從DelayQueue中獲取元素時,表示快取有效期到了。
      • 定時任務排程:使用DelayQueue儲存當天將會執行的任務和執行時間,一旦從DelayQueue中獲取到任務就開始執行,比如TimerQueue就是使用DelayQueue實現的。
    • SynchronousQueue:一個不儲存元素的阻塞佇列。它支援公平訪問佇列。預設情況下執行緒採用非公平性策略訪問佇列。適合傳遞性場景。吞吐量高於LinkedBlockingQueue和ArrayBlockingQueue。
      public SynchronousQueue(boolean fair) {
          transferer = fair new TransferQueue() : new TransferStack();
      }
      
    • LinkedTransferQueue:一個由連結串列結構組成的無界阻塞佇列。
    • LinkedBlockingDeque:一個由連結串列結構組成的雙向阻塞佇列。
  • 阻塞佇列的實現原理
    • 通知模式:當生產者往滿的佇列裡新增元素時會阻塞住生產者,當消費者消費了一個佇列中的元素後,會通知生產者當前佇列可用。
  • Fork/Join框架
    • 工作竊取演算法:是指某個執行緒從其他佇列裡竊取任務來執行。
      • 優點:充分利用執行緒進行平行計算,減少了執行緒間的競爭。
      • 缺點:在某些情況下還是存在競爭,比如雙端佇列裡只有一個任務時。並且該演算法會消耗了更多的系統資源,比如建立多個執行緒和多個雙端佇列。