源碼閱讀(35):Java中線程安全的Queue、Deque結構——LinkedBlockingQueue(1)

1、概述

之前花了大量的篇幅介紹了一個Java中線程安全的Queue結構:ArrayBlockingQueue。主要是爲了歸納分類這些線程安全性的Queue、Deque結構的設計共性。實際上ArrayBlockingQueue已經擁有了其它線程安全的Queue結構的大部分處理特點:

  • 基本上有界隊列都通過類似notEmpty和notFull這樣的java.util.concurrent.locks.Condition對象,來協調生產者線程和消費者線程的控制。

  • 基本上隊列都通過類似count這樣的變量,來記錄隊列中的數據總量。基本上有界隊列還要通過類似capacity這樣的變量來記錄隊列的容量上限;而無界隊列對於容量的記錄要求相對不嚴格,甚至沒有直接的變量進行容量上限的記錄。

  • 線程安全的阻塞性隊列,大多數通過可重入鎖ReentrantLock來保證多線程場景下的操作安全性(基於AQS方式),但也有線程安全的隊列使用CAS方式保證線程安全性。兩種保證線程安全新的技術使用其中一種就足夠了。

  • 爲了保證多線程操作場景下,多個迭代器的工作穩定性,這些隊列結構中的迭代器都做了較複雜的設計,其中ArrayBlockingQueue的迭代器又最具有代表性。

  • 爲了保證設計思路的可靠性,java原生的線程安全隊列的設計原理只有幾種:數組、鏈表(一般是單向的)、樹(一般是堆樹)。這樣做的目的主要是爲了承接基礎java集合框架(Java Collection Framework)的設計思想,以便於使用者進行源碼閱讀。

基於介紹ArrayBlockingQueue時我們描述的這些設計共性,本系列開始爲讀者介紹另一個重要的阻塞性隊列LinkedBlockingQueue。LinkedBlockingQueue是一種內部基於鏈表的,使用在高併發場景下的阻塞隊列,且是一種無界隊列。

2、LinkedBlockingQueue基本結構

下圖展示了LinkedBlockingQueue隊列的基本內部結構:
在這裏插入圖片描述
上圖即使讀者沒有接觸過LinkedBlockingQueue隊列的源代碼,也是非常容易理解的。請注意隊列中處於隊列頭部的節點,那個被head屬性引用的節點,這個節點(Node)中的item屬性始終不存儲數據對象(後文會進行詳細介紹)。

2.1、LinkedBlockingQueue中重要的屬性

從上圖中我們知道LinkedBlockingQueue隊列通過一個單向鏈表進行數據描述,其中LinkedBlockingQueue類中的head屬性指向單向鏈表的首節點,last變量指向單向鏈表的尾節點。capacity變量表示LinkedBlockingQueue隊列的容量上限…………,以下是LinkedBlockingQueue隊列中各種重要的屬性信息:

public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
  // ......
  // 該屬性代表當前LinkedBlockingQueue隊列的最大容量上限
  // 如果在LinkedBlockingQueue隊列初始化時沒有特別指定,就會被設置爲Integer.MAX_VALUE
  // 這樣一來LinkedBlockingQueue就相當於一個無界隊列
  /** The capacity bound, or Integer.MAX_VALUE if none */
  private final int capacity;
  // 當前LinkedBlockingQueue隊列中的數據量,爲什麼要使用基於CAS原理的AtomicInteger?
  // 究其原因是因爲LinkedBlockingQueue隊列的讀寫操作分別由兩個獨立的可重入鎖進行控制
  /** Current number of elements */
  private final AtomicInteger count = new AtomicInteger();
  // head指向單向鏈表的首節點,注意,head不會爲null
  transient Node<E> head;
  // last指向單向鏈表的尾節點,注意,laset也不會爲null
  // 有的時候head和last可能指向同一個節點。
  private transient Node<E> last;
  // 這個可重入鎖保證取出數據時的安全性,主要保證類似take, poll這樣的方法的取數正確性
  private final ReentrantLock takeLock = new ReentrantLock();
  // 這個Condition條件對象,當隊列中至少有一個數據時,進行通知
  @SuppressWarnings("serial") // Classes implementing Condition may be serializable.
  private final Condition notEmpty = takeLock.newCondition();
  / 這個可重入鎖保證添加數據時的安全性,主要保證類似put, offer這樣的方法的添加正確性
  private final ReentrantLock putLock = new ReentrantLock();
  // 這個Condition條件對象,當隊列中至少有一個空閒的可添加數據的位置時,進行通知
  @SuppressWarnings("serial") // Classes implementing Condition may be serializable.
  private final Condition notFull = putLock.newCondition();
  // ......
}

以上代碼片段在讀者閱讀了ArrayBlockingQueue的源代碼後,是不是就有了一些似曾相識的感覺?這就是因爲這兩個BlockingQueue結構的基本設計思路都是統一的。從以上代碼片段可以看出,後者和前者最大的區別除了一個使用單向鏈表結構,一個使用數組結構外,另外最大的區別就是後者的代碼中有兩個可重入鎖分別對數據添加過程和取數過程的多線程操作正確性進行控制。換句話說LinkedBlockingQueue的添加和取數過程應該是不衝突的,這在後續的內容中會進行詳細介紹

根據以上的描述,我們對之前LinkedBlockingQueue隊列內部結構的示意圖進行了細化:
在這裏插入圖片描述

2.2、LinkedBlockingQueue的構造函數

LinkedBlockingQueue一共有三個可用的構造函數,代碼片段如下所示:

public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
  // 這是默認的構造函數,其中調用了LinkedBlockingQueue(int)這個構造函數
  // 設定LinkedBlockingQueue隊列的最大熔鍊上限爲Integer.MAX_VALUE(相當於無界隊列)
  public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
  }

  // 該構造函數可以由調用者設定LinkedBlockingQueue隊列的容量上限
  // 如果設定的容量上限小於等於0,則會拋出異常
  public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) 
      throw new IllegalArgumentException();
    this.capacity = capacity;
    // 爲LinkedBlockingQueue隊列初始化一個單向鏈表,單向鏈表中只有一個Node節點,且這個節點沒有item數據
    last = head = new Node<E>(null);
  }

  // 該構造函數在完成LinkedBlockingQueue隊列初始化後,將一個外部集合C中的數據添加到LinkedBlockingQueue隊列中
  // 集合c不能爲null,否則會拋出異常;集合c中被取出的數據也不能爲null,否則同樣拋出異常。
  public LinkedBlockingQueue(Collection<? extends E> c) {
    // 進行LinkedBlockingQueue默認的初始化過程
    this(Integer.MAX_VALUE);
    final ReentrantLock putLock = this.putLock;
    // 獲取添加操作的操作權
    // 注意:由於是對象的實例化過程,所以這裏實際上不會有操作搶佔的問題
    // 但是基於整個LinkedBlockingQueue操作的規範性和代碼可讀性來說,是必要進行的
    putLock.lock();
    try {
      int n = 0;
      for (E e : c) {
        // 外部集合c中取出的數據不能爲null
        if (e == null) {
          throw new NullPointerException();
        }
        // 這句話可避免c中的數據總量大於Integer.MAX_VALUE
        // 基本上不會出現這種情況,但還是進行了限制
        if (n == capacity) {
          throw new IllegalStateException("Queue full");
        }
        // 依次進行數據的添加操作
        // enqueue方法和dequeue方法,下文中將立即進行介紹
        enqueue(new Node<E>(e));
        ++n;
      }
      // n代表初始化完成後,LinkedBlockingQueue隊列中的數據總量
      // 賦值爲AtomicInteger count屬性。
      count.set(n);
    } finally {
      putLock.unlock();
    }
  }
}

LinkedBlockingQueue隊列完成實例化後,最常見的情況如下圖所示:
在這裏插入圖片描述
基本上來說,如果在LinkedBlockingQueue隊列實例化過程中,沒有外部集合要求進行傳入,那麼實例化過程實際上就是初始化head、last屬性的過程——這兩個屬性將引用同一個Node節點。此時LinkedBlockingQueue隊列中只有這一個Node節點,且其中的item屬性爲null。實際上在後續的操作中,LinkedBlockingQueue隊列始終會保證在最前面的Node節點中,item屬性一直爲null

2.3、入隊和出隊操作

和前文已經介紹過的ArrayBlockingQueue隊列類似,LinkedBlockingQueue隊列的數據入隊操作和數據出隊操作,也是基於兩個私有方法來進行,且在進行這兩個私有方法操作時,調用者必須自行獲取隊列操作權限。這兩個私有方法分別是enqueue()方法和dequeue方法,下面本文就先行介紹這兩個私有方法:

  • enqueue(Node) 方法負責的入隊操作
// 這是一個Node節點的定義,其中包括兩個屬性:
// item:用於存儲當前節點的數據引用(可能爲null)
// next:指向當前節點的下一個節點(可能爲null)
static class Node<E> {
  E item;
  Node<E> next;
  Node(E x) { 
    item = x; 
  }
}

// 該私有方法負責進行數據入隊操作,既是在當前隊尾(last指向的節點之後)添加一個新的Node節點
// 該方法的調用者需要獲得兩個操作前提:
// 1、外部調用者所在線程必須已經獲取了putLock鎖
// 2、當前last中的next屬性值爲null
private void enqueue(Node<E> node) {
  // 只有這一句代碼
  last = last.next = node;
}

enqueue(Node)方法中只有一句代碼,主要過程是:首先將傳入的node節點引用至當前last的next屬性下,最後將last位置的指向向後移動,如下圖所示:
在這裏插入圖片描述

  • dequeue() 方法負責的出隊操作

由於ArrayBlockingQueue隊列使用的單向鏈表中,其head位置指向的Node節點中,item屬性都爲null。爲了保持這樣的結構特點,ArrayBlockingQueue中的dequeue()方法相比enqueue(Node)方法就要稍微複雜一點:

// 該方法負責出隊操作,既是把隊列頭部的數據移除隊列的操作
// 該操作同樣需要保證兩個前提條件:
// 1、外部調用者所在線程必須已經獲取了takeLock鎖
// 2、當前head位置的Node節點,其中的item屬性爲null
private E dequeue() {
  // 需要新創建兩個局部變量,輔助指向head位置,和head位置的下一個位置(後者可能爲null)
  Node<E> h = head;
  Node<E> first = h.next;
  // 將head.next屬性引用的位置指向自己,以幫助進行垃圾回收
  // 另外將節點的next屬性的引用指向自己,這種引用特性將幫助後介紹的迭代器位置校驗過程進行邏輯處理
  h.next = h; // help GC
  // 將當前head的位置向後移動
  head = first;
  // 從局部變量first所指向的Node中,獲得本次要出隊的數據對象
  // 並使當前first位置的Node節點,稱爲下一個head位置
  E x = first.item;
  first.item = null;
  // 返回獲取到的數據對象
  return x;
}

以上dequeue()方法的操作過程,可以用下圖進行表達:
在這裏插入圖片描述
這裏我們重點說明一下“h.next = h”這句代碼,這句代碼從字面的意義可以看出,是將head.next屬性的引用位置指向head對象本身,也就是指向當前隊列中的第一個節點(頭節點)。

我們知道,一般將對象的引用設定爲null。這樣一來,如果當前對象在內存中已經沒有強引用可達,那麼垃圾回收器就會回收這個對象。這裏需要注意的是,可達性是判定垃圾回收器是否回收對象的主要判定依據,爲了提高可達性的判定性能還加入了引用計數器,但是後者在對象不可達時,不一定非要降爲0。(有興趣的讀者可以自行查閱關於可達性和引用計數器的相關資料)

如上所示的“h.next = h”這個代碼片段,雖然h.next所指向的對象其引用計數器不爲0,但如果整個進程中從GC Roots開始該對象已經不可達,那麼這個對象也會被垃圾回收器回收。

========
(接後文《源碼閱讀(36):Java中線程安全的Queue、Deque結構——LinkedBlockingQueue(2)》)

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