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

1、LinkedTransferQueue概述

LinkedTransferQueue是從JDK 1.7+版本開始提供的一個無界阻塞式隊列,它是Java容器框架中一種比較特殊的阻塞式隊列,特殊性體現在它實現的TransferQueue接口。後者的特點是可定義一種數據對象消費者和生產者的配對交換方式,保證了生產者線程和消費者線程的配對處理(注意,不是數據配對而是線程配對),這樣做的好處是,可以使用CAS原理進行LinkedTransferQueue隊列集合的線程安全性控制,而不是使用AQS原理,在大多數高併發場景下,基於CAS工作的線程安全的數據結構其性能由於基於AQS工作的線程安全的數據結構。

從JDK 9+版本開始,LinkedTransferQueue內部的實現機制做了較大的調整,主要是使用變量句柄替代了
sun.misc.Unsafe工具類,並優化了內部結構的實現性能。本章節對於LinkedTransferQueue隊列集合的講解,將直接基於JDK 9+的源代碼版本。下圖是LinkedTransferQueue隊列集合的主要繼承體系:
在這裏插入圖片描述
LinkedTransferQueue隊列集合的使用場景同樣是基於生產者和消費者的多線程場景,在使用層面上該隊列集合和我們已經介紹過的ArrayBlockingQueue、LinkedBlockingQueue等隊列類似,如下所示(簡單的消費者和生產者示例):

// ......
public static void main(String[] args) {
  // 本文建議使用線程池管理線程,而不是直接創建Thread對象
  ThreadPoolExecutor serviceExecutor = new ThreadPoolExecutor(10,10,1000,TimeUnit.SECONDS,new LinkedBlockingQueue<>());
  // 這是一個LinkedTransferQueue隊列
  LinkedTransferQueue<String> queue = new LinkedTransferQueue<>();
  // 多個消費者和多個生產者
  serviceExecutor.submit(new Producer(queue));
  serviceExecutor.submit(new Producer(queue));
  serviceExecutor.submit(new Consumer(queue));
  serviceExecutor.submit(new Consumer(queue));
}

// 消費者
public static class Consumer implements Runnable {
  private TransferQueue<String> queue;
  
  public Consumer(TransferQueue<String> queue) {
    this.queue = queue;
  }
  
  @Override
  public void run() {
    int count = 0;
    while(count++ < Integer.MAX_VALUE) {
      try {
        String value = this.queue.take();
        System.out.println(value);
      } catch (InterruptedException e) {
        e.printStackTrace(System.out);
      }
    }
  }
}

// 生產者
public static class Producer implements Runnable {
  // 生產者生產的數據,將放入該隊列
  private TransferQueue<String> queue;
  public Producer(TransferQueue<String> queue) {
    this.queue = queue;
  }

  @Override
  public void run() {
    String uuid = UUID.randomUUID().toString();
    int count = 0;
    while(count++ < Integer.MAX_VALUE) {
      // 通過transfer的方式進行添加
      try {
        this.queue.transfer(uuid);
      } catch (InterruptedException e) {
        e.printStackTrace(System.out);
      }
    }
  }
}

請注意,和之前利用阻塞隊列實現的生產者/消費者進行比較,最有特點的就是生產者可以採用transfer將數據對象添加到LinkedTransferQueue隊列中,如果這個數據對象暫時沒有消費者線程取出進行處理,則當前生成者會進入阻塞狀態。而消費者依然可以使用類似的方式(take方法)試圖從隊列中取出數據對象,如果沒有數據對象可以取出,則消費者進入阻塞狀態。

2、LinkedTransferQueue核心結構

2.1、LinkedTransferQueue中的單向鏈表

LinkedTransferQueue內部採用一個無界單向鏈表來連續記錄需要從LinkedTransferQueue隊列取數據對象的消費者請求,或者向LinkedTransferQueue隊列添加數據對象的生產者請求。如下圖所示:
在這裏插入圖片描述
請注意,head引用位置不一定絕對在單向鏈表的首個Node對象上,tail引用位置也不一定絕對在單向鏈表的最後一個Node對象上(後續文章會重點講到這個問題)。如上圖所示,單向鏈表的每一個節點都是一個LinkedTransferQueue.Node類的實例,每一個Node類中不一定有數據,也不一定有消費者線程或者生產者線程的對象引用。Node類定義的代碼片段如下所示:

public class LinkedTransferQueue<E> extends AbstractQueue<E>
    implements TransferQueue<E>, java.io.Serializable {
    // ......
    // 該屬性記錄單向鏈表的第一個有效Node節點位置
    // 不一定是單向鏈表的頭節點
    transient volatile Node head;
    // 該屬性記錄單向鏈表的最新知曉的在多線程操作場景下正確入隊的最後一個Node節點位置
    // 不一定是單向鏈表的尾節點
    private transient volatile Node tail;
    
	// ......
	static final class Node {
	  // ......
	  // 表示該Node節點是否存儲了數據
	  // 如果沒有存儲數據,那麼說明這個節點記錄的是一個消費者請求
	  // 否則就是記錄的消費者請求
	  // false if this is a request node
	  final boolean isData;   
	  // 如果該節點記錄了數據,則數據對象通過該屬性被引用
	  // initially non-null if isData; CASed to match
	  volatile Object item;
	  // 該屬性指向當前節點的下一個節點,以便構成一個鏈表
	  volatile Node next;
	  // 造成當前節點所記錄的請求,對應的線程
	  // null when not waiting for a match
	  volatile Thread waiter; 
	  // ......
	}
	// ......
}

注意,正常情況下Node對象中的isData屬性和item屬性所描述的結論應該是一致的,也就是說當isData屬性值爲false時,item屬性值應該爲null;當isData屬性值爲true時,item屬性值應該不爲null。但是在單向鏈表工作過程中,經常會出現isData屬性和item屬性描述結論不一致的情況,這是正常情況,這種節點稱爲“虛”節點(或無效節點)。

當一個消費者線程向LinkedTransferQueue請求取得數據時,如果LinkedTransferQueue隊列中沒有任何可以取出的數據,那麼將爲這個取數請求創建一個節點,這個節點將會被添加到當前鏈表的末尾,同時這個取數請求的線程將進入阻塞狀態(parking_waiting)。當然根據調用方法不同,取數請求的線程也有可能不會被阻塞,但是這於核心原理並沒有本質影響。

隨後,當一個生產者線程向LinkedTransferQueue請求添加數據時,會從單向鏈表的head節點開始匹配,如果發現某個節點是一個取數請求任務類型的節點(即是這個節點的isData爲false,item == null),那麼則將數據添加到這個節點上,並通知取數請求的線程,解除阻塞狀態。如下圖所示:
在這裏插入圖片描述
注意,head引用位置上的節點,可能是一個“虛”節點,這時處理過程會繼續向後尋找,直到匹配到第一個有效的任務節點。

並且,以上處理規則是可以反過來的: 當一個生產者線程向LinkedTransferQueue請求添加數據時,如果LinkedTransferQueue隊列中沒有任何等待取數的請求節點,那麼將爲這個生產者的添加請求創建一個節點,這個節點將會被添加到當前鏈表的末尾,同時這個添加請求的線程將視調用情況進入阻塞狀態(parking_waiting)…… 如下圖所示:
在這裏插入圖片描述
請注意,以上處理邏輯隱含了一個潛在規則,就是LinkedTransferQueue內部鏈表上的有效節點,要麼全部都是由取數請求創建的節點,其isData爲false,item屬性爲null;要麼就全部都是由存儲請求創建的節點,其isData爲true,item屬性不爲null。這就是爲什麼與之對應的消費者或者生產者,都只需要由head開始找到第一個有效節點判定是否可以存儲/添加數據,而不需要對這個鏈表上的所有節點性質進行判定的原因。

經過對LinkedTransferQueue處理邏輯的初步分析,我們可以得出以下幾個顯而易見的結論:

  • 基於LinkedTransferQueue工作的生產者和消費者,其添加/取出數據的理論時間複雜度爲O(1)

  • LinkedTransferQueue隊裏集合採用CAS思想而非AQS思想保證自身的線程安全性。

  • 要保證LinkedTransferQueue的線程安全性,本質上就是在多線程(多生產者多消費者)的場景下維護這個單向鏈表。要知道實際的工作場景下,有多個生產者線程和消費者線程同時發起對這個單向鏈表的操作,如下圖所示:
    在這裏插入圖片描述

2.2、VarHandle變量句柄

從JDK 9+開始,Java提供了一個VarHandle變量句柄功能,用以推薦給程序員支持自行編寫的CAS思路的程序邏輯落地。在這之前,JDK內部實現CAS思想時,大量使用了UnSafe工具類。後者這是一個直連Hotspot VM後門的工具類(在前文中我們已經介紹過《多任務處理(17)——Java中的鎖(Unsafe基礎)》),如果不修改虛擬機的安全性規則,編譯器不允許程序員直接使用這個類,並且一般情況下程序員也需要使用“反射”的方式獲得UnSafe工具類的操作對象。

2.2.1、利用java.util.concurrent.atomic包完成原子操作

Unsafe工具類中基本都是基於JIN直接對內存進行的操作,或者說直接通過C對硬件進行的操作。Java語言的各個版本設計,都不希望程序員衝破這個封裝,所以纔會對UnSafe工具類的使用做諸多限制。在JDK 9 之前技術人員要自行完成CAS思想的落地實際上除了使用UnSafe工具類外,實際上就是使用java.util.concurrent.atomic原子操作包提供的各種工具類——例如技術人員要利用CAS思想在多線程場景下,完成某個對象屬性的線程安全性的賦值操作,那麼通常還有以下這些選擇(可選擇的方式確實算不上豐富):

  • 使用java.util.concurrent.atomic包下的某個原子操作工具,輔助完成CAS判定過程,例如使用AtomicBoolean模擬搶佔與自旋操作。基本思路是:多線程場景下,成功將AtomicBoolean對象從false賦值爲true的線程,視爲搶佔成功的線程,可以進行對象屬性的賦值操作,其它線程進入“自旋”。代碼格式通常如下:
// 這是多個線程搶着要使用的資源
private XXXXX filed1;
// 這是輔助完成cas判定的原子操作工具
private AtomicBoolean isOk = new AtomicBoolean(false);

// .....
for(;;) {
  // 如果條件成立,說明當前線程搶佔到了資源操作權
  // 此方法的意思是,如果當前isOk的布爾值爲false,則將isOk的布爾值設置爲true,並返回原子操作成功的信息。
  // 否則返回false,代表原子操作失敗
  if(isOk.compareAndSet(false , true)) {
    try {
      // 這裏進行filed1賦值操作和業務邏輯處理
      // .........
      return;
    } finally {
      // 無論處理是否成功,業務操作後,都要將isOk設置爲false
      // 以便其它還在自旋的線程能夠繼續利用CAS的思想進行競爭
      isOk.set(false);
    }
  }
}
// .....

這種處理選擇的問題在於所需的代碼段落比較多,實際上AtomicBoolean內部就是UnSafe工具類的封裝。

  • 另外還有一種輔助完成CAS的方式,是藉助java.util.concurrent.atomic包中的AtomicReference對象,完成被引用的對象更新。

之前的文章已經介紹過,AtomicReference和AtomicBoolean、AtomicInteger等原子操作類工作原理類似,不同的是前者可以對更泛化的對象引用進行原子操作,而不像後者那樣,只是基於特定的boolean、int基礎類型進行操作。 基本的使用模式可以如下所示:

public class ...... {
  // 這是多個線程搶着要使用的資源
  private YYYY filed1 = new YYYY();
  
  // ......
  // 建立filed1屬性的原子操作引用
  AtomicReference atomicReference = new AtomicReference(filed1);
  // .....
}

在JDK 9之前的版本,AtomicReference內部還是基於UnSafe工具類的封裝,但是在JDK9 +的版本開始,AtomicReference內部的封裝改爲了VarHandle變量句柄。

  • 還可以藉助java.util.concurrent.atomic包中諸如AtomicIntegerFieldUpdater這樣的字段原子更新工具或者使用支持更泛化類型的AtomicReferenceFieldUpdater原則更新工具。

這個使用方式在之前的文章中也進行了介紹,這裏就不再贅述,基礎思想也是對UnSafe工具類的封裝。由此可見如果技術人員遵循Java官方要求,。所以。通過以上這些介紹,讀者應該可以更加明確的知道爲什麼java.util.concurrent.atomic包叫做原子操作包了

2.2.2、利用VarHandle變量句柄完成工作

從JDK 9+開始,如果開發人員遵循Java推薦規範不直接使用UnSafe工具類來落地自己的CAS實現,那麼除了使用java.util.concurrent.atomic原子操作包外,還可以使用新提供的VarHandle變量句柄。VarHandle變量句柄不止是Java推薦開發人員使用的落地CAS的方式,JDK內部也在逐漸使用VarHandle變量句柄的方式,替換掉以前大規模直接使用的UnSafe工具類。

例如AtomicReference的內部、前文介紹的PriorityBlockingQueue隊列集合;再例如,本文正在介紹的LinkedTransferQueue隊列,其源代碼中也充分利用了VarHandle變量句柄完成CAS過程,其中和VarHandle直接有關的定義如下:

// ......
public class LinkedTransferQueue<E> extends AbstractQueue<E>
    implements TransferQueue<E>, java.io.Serializable {
  // ......
  // VarHandle mechanics
  // 爲以下屬性建立操作句柄,以簡化CAS的編碼過程。
  private static final VarHandle HEAD;
  private static final VarHandle TAIL;
  private static final VarHandle SWEEPVOTES;-
  static final VarHandle ITEM;
  static final VarHandle NEXT;
  static final VarHandle WAITER;
  static {
    try {
      MethodHandles.Lookup l = MethodHandles.lookup();
      // 建立LinkedTransferQueue類中,head屬性的操作句柄
      HEAD = l.findVarHandle(LinkedTransferQueue.class, "head", Node.class);
      // tail屬性的操作句柄
      TAIL = l.findVarHandle(LinkedTransferQueue.class, "tail", Node.class);
      // sweepVotes屬性的操作句柄
      SWEEPVOTES = l.findVarHandle(LinkedTransferQueue.class, "sweepVotes", int.class);
      // LinkedTransferQueue.Node類中,item屬性的操作句柄
      ITEM = l.findVarHandle(Node.class, "item", Object.class);
      // LinkedTransferQueue.Node類中,next屬性的操作句柄 
      NEXT = l.findVarHandle(Node.class, "next", Node.class);
      // LinkedTransferQueue.Node類中,waiter屬性的操作句柄
      WAITER = l.findVarHandle(Node.class, "waiter", Thread.class);
    } catch (ReflectiveOperationException e) {
      throw new ExceptionInInitializerError(e);
    }
    // ......
  }
  // ......
}
// ......

LinkedTransferQueue隊列主要通過CAS原理保證多線程場景下內部單向鏈表結構的數據準確性,而進行CAS思想落地的主要方式就是依靠VarHandle變量句柄。

========(接下文)

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