生產-消費模型之"阻塞隊列"的源碼分析

前言

通常來說,Queue隊列的特性一般爲FIFO(先進先出),在隊列的特性上在增加一個阻塞的特性,這就是BlockingQueue,這很像一個生產者-消費者模式,生產者負責將某些元素放入隊列,消費者負責從隊列中取元素,阻塞-通知的特性讓這種模式更加高效,例如有元素我就會喚醒阻塞的線程,而不是一直去主動輪詢是否有任務,在線程池中也利用了阻塞隊列的特性,實現了線程任務的提交和獲取執行,所以配置一個合適的線程池之前,你需要了解阻塞隊列的實現和使用。

本篇文章的議題圍繞BlockingQueue幾個常用的阻塞隊列實現類:

  • SynchronousQueue
  • ArrayBlockingQueue
  • LinkedBlockingQueue

但阻塞隊列中有些方法是會一直阻塞,有些方法又不會,每個方法都有其特有的場景和作用,在這裏會介紹其源碼,分析其原理,讓讀者更好的使用和了解阻塞隊列的特性。

不同阻塞隊列實現類,內部的數據結構和行爲都存在一定的不同,所以本篇文章的維度將在不同阻塞隊列的實現和實現對應的一系列存取方法來展開。

阻塞隊列API

那麼阻塞隊列都有哪些方法可供用戶使用呢?直接來看J.U.C中的BlockingQueue這個接口

存放元素

boolean add(E e)

Inserts the specified element into this queue if it is possible to do so immediately without violating capacity restrictions, returning {@code true} upon success and throwing an {@code IllegalStateException} if no space is currently available.
When using a capacity-restricted queue, it is generally preferable to use {@link #offer(Object) offer}.

此方法上的註釋說明了此方法的特性:

  • 會立即返回
    • 返回true代表添加成功
    • 拋出IllegalStateException異常代表容量限制
  • 如果是一個有容量限制的隊列,一般來說更偏向使用offer方法去存放元素

boolean offer(E e)

Inserts the specified element into this queue if it is possible to do so immediately without violating capacity restrictions, returning {@code true} upon success and {@code false} if no space is currently available. When using a capacity-restricted queue, this method is generally preferable to {@link #add}, which can fail to insert an element only by throwing an exception.

  • 立即返回
    • 返回ture代表添加成功
    • 返回false代表添加失敗,當前沒有可用空間
  • 在有容量限制的隊列中,通常這個方法比add要好

boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;

Inserts the specified element into this queue, waiting up to the specified wait time if necessary for space to become available.

  • 會阻塞一段用戶自定的timeout時間,爲阻塞一段時間版的offer
    • 因爲會阻塞,所以有可能拋出InterruptedException中斷的異常

void put(E e) throws InterruptedException

Inserts the specified element into this queue, waiting if necessary for space to become available.

  • 如果隊列沒有空間存放元素會一直阻塞
    • 若方法返回,代表添加成功
    • 因爲會阻塞,所以此方法有可能拋出InterruptedException中斷的異常

獲取元素

E take() throws InterruptedException

Retrieves and removes the head of this queue, waiting if necessary until an element becomes available.

  • 取出然後刪除隊列的頭元素
  • 如果沒有元素在隊列,方法會一直阻塞
    • 同樣需要處理中斷異常

E poll(long timeout, TimeUnit unit) throws InterruptedException

Retrieves and removes the head of this queue, waiting up to the specified wait time if necessary for an element to become available.

  • 取出然後刪除隊列頭元素,與上面方法的不同點在於此方法指定了阻塞的時間

小結

這裏對阻塞隊列的API做一個小結

存數據操作 是否會阻塞 是否可以指定阻塞時間
add(E e) 不會 不可以
offer(E e) 不會 不可以
offer(E e, long timeout…) 可以
put(E e) 不可以

例如,我們需要只阻塞一段時間,那就需要使用帶時限的offer方法,若想要阻塞直到可以放入元素,那就需要put或者帶時限的offer,如果不想阻塞,只想嘗試put一下,可以使用offer方法,add方法不推薦使用。

取數據操作 是否會阻塞 是否可以指定阻塞時間
take() 不可以
poll(long timeout…) 可以

取元素操作相對比較簡單一些,若不想要阻塞,只是嘗試獲取,可以使用poll(0)

ArrayBlockingQueue

這個阻塞隊列是一個有界隊列,其界限特性在其構造函數中就可以看出

public ArrayBlockingQueue(int capacity) {
  this(capacity, false);
}

public ArrayBlockingQueue(int capacity, boolean fair) {
  if (capacity <= 0)
    throw new IllegalArgumentException();
  // 內部的數據結構,數組
  this.items = new Object[capacity];
  // 阻塞的特性的實現,類似獲取特定鎖下的wait-notify操作
  lock = new ReentrantLock(fair);
  notEmpty = lock.newCondition();
  notFull =  lock.newCondition();
}

由此可見,在創建一個ArrayBlockingQueue時必須指定一個容量,隊列內部的數據結構則爲一個數組來存放

/** The queued items */
final Object[] items;

很簡單,在隊列初始化之後會分配一段連續的內存(數組),以數組作爲其數據結構。下面來看看開頭我們提到的API都是怎麼實現的

其中add方法爲父類AbstractQueue的模版方法

public boolean add(E e) {
  if (offer(e))
    return true;
  else
    throw new IllegalStateException("Queue full");
}

其實就只是offer而已,只不過add會拋出異常,建議使用offer方法,省去處理異常這一步

offer(無時限)

public boolean offer(E e) {
  checkNotNull(e);
  final ReentrantLock lock = this.lock;
  // 因爲要使用condition(wait-notify)特性,所以需要獲取鎖
  // 重複邏輯下面都會出現,不再贅述
  lock.lock();
  try {
    // 當前容量等於數組長度,代表隊列滿了,直接返回false
    if (count == items.length)
      return false;
    else {
      // 元素入隊
      enqueue(e);
      return true;
    }
  } finally {
    lock.unlock();
  }
}

很簡單,由於offer(E e)方法不具有阻塞特性,所以在隊列滿的時候直接返回false

offer(有時限)

public boolean offer(E e, long timeout, TimeUnit unit)
  throws InterruptedException {

  checkNotNull(e);
  long nanos = unit.toNanos(timeout);
  final ReentrantLock lock = this.lock;
  // 值得一提,這裏可響應中斷
  lock.lockInterruptibly();
  try {
    // 如果隊列滿了
    while (count == items.length) {
      if (nanos <= 0)
        return false;
      // 則等待一個限定的時間
      nanos = notFull.awaitNanos(nanos);
    }
    // 入隊
    enqueue(e);
    return true;
  } finally {
    lock.unlock();
  }
}

沒什麼好說的,時限等待是通過Condition的實現來做的。這裏enqueue是通用入隊方法,在後面再詳細分析

put(E e)

public void put(E e) throws InterruptedException {
  checkNotNull(e);
  final ReentrantLock lock = this.lock;
  lock.lockInterruptibly();
  try {
    // 如果隊列滿了
    while (count == items.length)
      // 類似wait方法,等待有線程向隊列添加元素,就會喚醒
      notFull.await();
    // 入隊
    enqueue(e);
  } finally {
    lock.unlock();
  }
}

看到這裏,可以看出來,其入隊的重點就在enqueue方法中

private void enqueue(E x) {
  // assert lock.getHoldCount() == 1;
  // assert items[putIndex] == null;
  final Object[] items = this.items;
  // putIndex是一個遊標
  items[putIndex] = x;
  if (++putIndex == items.length)
    // 當隊列滿時重置爲0,因爲是數組,且先進先出的原則
    putIndex = 0;
  count++;
  // 喚醒阻塞在因爲線程爲空而獲取不到元素的線程
  notEmpty.signal();
}

取元素的操作例如poll、take方法也都是很簡單的,讀者可以自行查看,關鍵分析dequeue方法

private E dequeue() {
  // assert lock.getHoldCount() == 1;
  // assert items[takeIndex] != null;
  final Object[] items = this.items;
  @SuppressWarnings("unchecked")
  // 獲取此時遊標上對應的元素
  E x = (E) items[takeIndex];
  // 注意需要被remove
  items[takeIndex] = null;
  if (++takeIndex == items.length)
    takeIndex = 0;
  count--;
  if (itrs != null)
    itrs.elementDequeued();
  // 喚醒那些因爲隊列滿了而存不進元素阻塞的線程
  notFull.signal();
  return x;
}

小結

其阻塞特性,大致是如下方式實現的:

  • 取元素時若沒元素,則在notEmpty這個Condition上等待,取完元素就會在notFull這個Condition上喚醒線程
  • 存元素時若沒元素,則在notFull這個Condition上等待,存完元素就會在notEmpty這個Condition上喚醒線程

可以看到,其使用了兩個條件變量Condition,去控制阻塞的行爲,很好的封裝了Condition的使用,使得我們可以在生產-消費模型中直接拿來使用

ArrayBlockingQueue阻塞隊列總結如下:

  • 有界隊列,需要指定大小
  • 數組結構,初始化時需要分配一段連續的內存

LinkedBlockingQueue

老樣子,先看看其構造函數的實現

public LinkedBlockingQueue() {
  this(Integer.MAX_VALUE);
}

public LinkedBlockingQueue(int capacity) {
  if (capacity <= 0) throw new IllegalArgumentException();
  this.capacity = capacity;
  // 初始化一個Node
  last = head = new Node<E>(null);
}

可以看到,如果在構造函數中指定一個容量,則此隊列就是有界的,如果沒有指定容量,可以視爲無界隊列。

其使用AtomicInteger來存放容量

/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();

其數據結構爲一串單向鏈表

static class Node<E> {
  // 元素內容
  E item;
  // 下一個節點
  Node<E> next;
  Node(E x) { item = x; }
}

以put爲例看看存放元素是如何實現的

public void put(E e) throws InterruptedException {
  if (e == null) throw new NullPointerException();
  int c = -1;
  // 構造一個Node節點
  Node<E> node = new Node<E>(e);
  final ReentrantLock putLock = this.putLock;
  // 獲取容量
  final AtomicInteger count = this.count;
  putLock.lockInterruptibly();
  try {
    // 滿了的話就阻塞
    while (count.get() == capacity) {
      notFull.await();
    }
    // 入隊
    enqueue(node);
    // 增加容量
    c = count.getAndIncrement();
    // 當前容量是否小於界限容量
    if (c + 1 < capacity)
      notFull.signal();
  } finally {
    putLock.unlock();
  }
  if (c == 0)
    signalNotEmpty();
}

阻塞特性依舊使用Condition實現,不多贅述,存取元素的關鍵都在enqueue、dequeue方法

private void enqueue(Node<E> node) {
  // 在尾節點的next塞節點
  last = last.next = node;
}
private E dequeue() {
  // assert takeLock.isHeldByCurrentThread();
  // assert head.item == null;
  Node<E> h = head;
  Node<E> first = h.next;
  h.next = h; // help GC
  head = first;
  E x = first.item;
  first.item = null;
  return x;
}

很簡單,入隊不過是在尾部next增加一個節點,出隊不過是取出頭節點

小結

這裏就不做過多分析了,都是一些重複的邏輯,其阻塞特性也是使用了兩個Condition去實現的。

LinkedBlockingQueue阻塞隊列總結如下:

  • 可有界可無界,由構造函數的參數決定
  • 鏈表結構,好處在於初始化時不用向數組那樣要先分配一段連續的內存

SynchronousQueue

接下來就是最難的傳球手隊列,其本質不存放元素,所以它的存取方法都比較有特點。先來看看構造函數

public SynchronousQueue() {
  // 非公平棧
  this(false);
}

public SynchronousQueue(boolean fair) {
  // 默認爲非公平的實現
  transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}

transferer變量是實現存取的關鍵,其默認使用非公平實現,也就是TransferStack

此隊列幾個存取方法都一樣調用了transferer變量的transfer方法,例如offer方法

public boolean offer(E e) {
  if (e == null) throw new NullPointerException();
  return transferer.transfer(e, true, 0) != null;
}

只不過transfer方法的參數不同,這裏總結一個表格,然後詳細分析transfer方法,期間讀者可以對照表格來看看transfer方法的一系列不同行爲

存取方法 transfer方法調用格式
put(E e) transferer.transfer(e, false, 0)
offer(E e, long timeout…) transferer.transfer(e, true, unit.toNanos(timeout)
offer(E e) transferer.transfer(e, true, 0)
take() transferer.transfer(null, false, 0)
poll(long timeout…) transferer.transfer(null, true, unit.toNanos(timeout))
poll() transferer.transfer(null, true, 0)

可以發現一個規律,一定會阻塞的方法在第二個參數都爲false,不會阻塞或只阻塞一段時間的第二個參數都爲true,第三個參數則爲阻塞限定時間(如果有的話),第一個參數爲null表示是取元素

在分析之前,先來看一下關鍵的transferer對象的結構:

/** Dual stack */
static final class TransferStack<E> extends Transferer<E> {
  
  // 有三種模式
  /* Modes for SNodes, ORed together in node fields */
  /** Node represents an unfulfilled consumer */
  // 請求數據模式,例如take
  static final int REQUEST    = 0;
  /** Node represents an unfulfilled producer */
  // 插入數據模式,例如put
  static final int DATA       = 1;
  /** Node is fulfilling another unfulfilled DATA or REQUEST */
  static final int FULFILLING = 2;
  
  // 其還有一個SNode對象,作爲棧結構的頭節點
  /** The head (top) of the stack */
  volatile SNode head;
}

還有一個關鍵對象,就是上面代碼裏的head的那個SNode對象:

static final class SNode {
  // 可以看出,雖說是棧結構,其內部也很像一個單向鏈表
  // 這裏保存了一個指向了下一個節點的引用
  volatile SNode next;        // next node in stack
  volatile SNode match;       // the node matched to this
  // 保存該節點所屬的線程,爲了喚醒線程所以需要保存一個
  volatile Thread waiter;     // to control park/unpark
  // 如果爲null,表示此節點是取模式,如果有數據,表示此節點是存模式
  Object item;                // data; or null for REQUESTs
  // 模式,在TransferStack對象中有聲明
  int mode;
}

有一個線程調用put方法

爲了降低複雜度,首先我們來模擬一個流程,假設有一個線程正在對隊列調用put方法,準備插入元素。由上面的表格可知,put方法的參數爲e, false, 0,分別表示:插入元素、不超時(一直阻塞)、無超時時間

// e, false, 0
E transfer(E e, boolean timed, long nanos) {
  SNode s = null; // constructed/reused as needed
  // mode = DATA 表示插入模式
  int mode = (e == null) ? REQUEST : DATA;

  for (;;) {
    // 此時是剛開始,所以 h = head = null
    SNode h = head;
    // 進入此分支
    if (h == null || h.mode == mode) {  // empty or same-mode
      // 超時判斷在這裏體現,timed就代表是否有超時限制,有超時限制且nanos=0在此就會立即返回了
      // 這裏爲false,則代表沒有超時的限制
      if (timed && nanos <= 0) {      // can't wait
        if (h != null && h.isCancelled())
          casHead(h, h.next);     // pop cancelled node
        else
          return null;
      } 
      // 進入這條分支,對當前put請求構造一個SNode對象
      // 當前SNode表示item=e,next=null,mode=存模式
      // 然後將此SNode設置爲head
      else if (casHead(h, s = snode(s, e, h, mode))) {
        // 阻塞線程
        SNode m = awaitFulfill(s, timed, nanos);
        // 這裏埋下伏筆,如果m和s相等,代表節點此時是被取消或中斷了
        // 所以如果要取消一個節點的等待,可以喚醒並將awaitFulfill返回值設置爲自身
        if (m == s) {               // wait was cancelled
          clean(s);
          return null;
        }
        if ((h = head) != null && h.next == s)
          casHead(h, s.next);     // help s's fulfiller
        // 因爲是插入模式 DATA,所以此時返回s節點的item
        return (E) ((mode == REQUEST) ? m.item : s.item);
      }
    } 
    // 省略無關代碼路徑...
  }
}

此時的流程如下所示:

  1. 沒有超時限制,則會走下面流程(因爲此時僅僅只有put方,沒有take方)
  2. 構造一個SNode,將當前節點設置爲頭節點head,模式爲存DATA模式
  3. awaitFulfill方法阻塞當前線程
  4. 直到有取節點時,會喚醒當前阻塞的線程,然後就可以返回了

其中3、4的步驟還是比較模糊的,其中第四個步驟在分析了take流程就懂了,下面來分析一下第三個步驟,awaitFulfill方法如何阻塞線程

// s爲上面構造好的SNode,timed=false,nanos=0
SNode awaitFulfill(SNode s, boolean timed, long nanos) {
  // 從這裏可以看出,如果timed=true有超時限制,此時就會計算一個超時時間
  final long deadline = timed ? System.nanoTime() + nanos : 0L;
  Thread w = Thread.currentThread();
  // 判斷是否需要自旋
  int spins = (shouldSpin(s) ?
               (timed ? maxTimedSpins : maxUntimedSpins) : 0);
  for (;;) {
    // 檢查中斷標誌
    if (w.isInterrupted())
      // 如果被中斷,需要取消節點
      // 剛剛也說了,取消節點其實就是將當前SNode對象放入自身的match變量中
      s.tryCancel();
    SNode m = s.match;
    // 如果match變量不爲null,有兩種可能,其中一種就是上面說的中斷
    if (m != null)
      // 如果是中斷,此時會返回當前SNode
      return m;
    // 有超時限制
    if (timed) {
      nanos = deadline - System.nanoTime();
      if (nanos <= 0L) {
        // 超過了超時時間,直接取消
        s.tryCancel();
        continue;
      }
    }
    // 如果需要自旋,則自旋參數-1,繼續循環
    if (spins > 0)
      spins = shouldSpin(s) ? (spins-1) : 0;
    // 接下來即將阻塞線程,所以把當前線程放入SNode變量waiter中,方便後面其他線程可以喚醒本線程
    else if (s.waiter == null)
      s.waiter = w; // establish waiter so can park next iter
    // 沒有超時限制,就直接阻塞
    else if (!timed)
      LockSupport.park(this);
    // 如果有超時限制,阻塞一個限定的時間
    else if (nanos > spinForTimeoutThreshold)
      LockSupport.parkNanos(this, nanos);
  }
}

到這裏可以看出幾個特點:

  1. 自旋特性(當前節點爲頭節點就有可能自旋)
  2. 跳出阻塞圈的關鍵就在SNode的match變量是否爲null,這裏我們只看到了取消節點時跳出循環的情況,還沒有看到別的情況,暫時存疑,在後面揭曉
  3. 調用LockSupport的阻塞方法,阻塞當前線程

到這裏,我們假設當前線程就已經被阻塞住了,接下來我們繼續假設,又有一個線程調用了同樣的put方法,那麼會發生什麼呢?

E transfer(E e, boolean timed, long nanos) {
  SNode s = null; // constructed/reused as needed
  // DATA
  int mode = (e == null) ? REQUEST : DATA;

  for (;;) {
    // 此時head爲剛剛的SNode
    SNode h = head;
    // head的mode顯然和此時的put一樣,進入該分支
    if (h == null || h.mode == mode) {  // empty or same-mode
      // 無超時限制
      if (timed && nanos <= 0) {      // can't wait
        if (h != null && h.isCancelled())
          casHead(h, h.next);     // pop cancelled node
        else
          return null;
      }
      // 同樣是進入此分支,構造當前SNode成爲head節點
      else if (casHead(h, s = snode(s, e, h, mode))) {
        // 同樣阻塞等待
        SNode m = awaitFulfill(s, timed, nanos);
        if (m == s) {               // wait was cancelled
          clean(s);
          return null;
        }
        if ((h = head) != null && h.next == s)
          casHead(h, s.next);     // help s's fulfiller
        return (E) ((mode == REQUEST) ? m.item : s.item);
      }
    } 
    // ...
  }
}

可以看到,第二個線程的put也會導致等待,值得一提的是第二個線程成爲了head,就像入棧一樣的操作。此時類結構如下圖所示在這裏插入圖片描述
可以假設,如果後面再有線程進來put,重複入棧操作,成爲head

另一個線程調用take方法

假設此時有一條線程想要從隊列中取元素,於是它調用了transferer.transfer(null, false, 0)

// null, false, 0
E transfer(E e, boolean timed, long nanos) {
  SNode s = null; // constructed/reused as needed
  // REQUEST
  int mode = (e == null) ? REQUEST : DATA;

  for (;;) {
    // B Snode
    SNode h = head;
    // 模式不一樣且不爲null,不進入此分支
    if (h == null || h.mode == mode) {  // empty or same-mode
       // ...
    } 
    // 進入此分支
    else if (!isFulfilling(h.mode)) { // try to fulfill
      if (h.isCancelled())            // already cancelled
        casHead(h, h.next);         // pop and retry
      // 假設沒有被取消,此時會構造一個SNode,next=剛剛的head,模式爲FULFILLING
      else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
        for (;;) { // loop until matched or waiters disappear
          // m = 剛剛的 head = B SNode
          SNode m = s.next;       // m is s's match
          if (m == null) {        // all waiters are gone
            casHead(s, null);   // pop fulfill node
            s = null;           // use new node next time
            break;              // restart main loop
          }
          // mn = A SNode
          SNode mn = m.next;
          // unpark B SNode對應的線程,下面會提到
          if (m.tryMatch(s)) {
            // 將A SNode 設置爲head
            casHead(s, mn);     // pop both s and m
            // mode沒有被修改過,此時mode = REQUEST,返回 m的item也就是B SNode的元素
            return (E) ((mode == REQUEST) ? m.item : s.item);
          } else                  // lost match
            s.casNext(m, mn);   // help unlink
        }
      }
    } else {                            // help a fulfiller
      // ...
    }
  }
}

以上流程可以簡化如下:

  1. 喚醒剛剛的head也就是B SNode,並取其item(要傳遞的元素)給當前take線程
  2. 將A SNode設置爲head
  3. 如果下面還有線程過來take,以此類推還會喚醒A SNode,然後把head設置爲null表示沒有元素了

以上流程唯一的疑點就在喚醒線程的tryMatch方法,從註釋中可以看出,是B SNode調用了此方法,參數爲take線程的那個SNode

boolean tryMatch(SNode s) {
  // 判斷是否match過,沒有衝突即爲null
  // 然後將當前take線程的SNode置換爲B SNode的match變量,這樣B線程就可以從之前那個阻塞的循環退出了
  if (match == null &&
      UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) {
    // 取出B線程
    Thread w = waiter;
    if (w != null) {    // waiters need at most one unpark
      waiter = null;
      // 喚醒B線程
      LockSupport.unpark(w);
    }
    return true;
  }
  return match == s;
}

很簡單,此方法主要就是喚醒了線程,並且將SNode的match設置了一下,這樣可以讓B線程從awaitFulfill方法中的循環中退出,忘記了的讀者可以回憶一下這個方法。此時隊列狀態如下
在這裏插入圖片描述
這個時候聰明的讀者可能已經發現了,無論是非阻塞的poll方法還是阻塞一段時間的poll還是剛剛的阻塞方法take,其實在head節點不爲null也就是已經有線程在等待put元素的前提下都是一樣的,換句話說,transfer(E e, boolean timed, long nanos)方法的三個參數,此時只有第一個參數有用,表示是REQUEST取模式,那麼後兩個參數有什麼用呢?可以假設此時棧無元素,head=null,假設此時有線程來取元素,調用了poll(E e) 不阻塞的方法

// null, true, 0
E transfer(E e, boolean timed, long nanos) {
  
  SNode s = null; // constructed/reused as needed
  // REQUEST
  int mode = (e == null) ? REQUEST : DATA;

  for (;;) {
    SNode h = head;
    // head=null,進入此分支
    if (h == null || h.mode == mode) {  // empty or same-mode
      // time=true,有超時限制且超時時間爲0,表示不阻塞
      if (timed && nanos <= 0) {      // can't wait
        if (h != null && h.isCancelled())
          casHead(h, h.next);     // pop cancelled node
        else
          // 直接返回null
          return null;
      } 
    } 
    // ...
  }
}

可以看到,此時是會直接返回的,這個情況放在不阻塞的offer方法也是如此,從開頭表格來看,offer方法的調用簽名爲transferer.transfer(e, true, 0)

// e, true, 0
E transfer(E e, boolean timed, long nanos) {
  SNode s = null; // constructed/reused as needed
  int mode = (e == null) ? REQUEST : DATA;

  for (;;) {
    SNode h = head;
    // 進入此分支,因爲head=null
    if (h == null || h.mode == mode) {  // empty or same-mode
      // timed=true,有超時限制且超時時間爲0
      if (timed && nanos <= 0) {      // can't wait
        if (h != null && h.isCancelled())
          casHead(h, h.next);     // pop cancelled node
        else
          // 直接返回null
          return null;
      } 
      //...
  }
}

由此我們可以總結SynchronousQueue這個隊列的幾個入隊出隊特性:

  • 取元素
    • 阻塞
      • take:一直阻塞到有線程put元素爲止,若多線程take,則依次入棧(不公平)
      • poll(long timeout…):一直阻塞直到有線程put元素或者等待了timeout就會喚醒
    • 非阻塞
      • poll():如果沒有線程put,直接返回,此方法需要有線程在阻塞put,纔可以獲取到元素
  • 存元素
    • 阻塞
      • put:一直阻塞到有線程take元素爲止,若多線程put,則依次入棧(不公平)
      • offer(long timeout…):一直阻塞直到有線程take元素或者等待了timeout就會喚醒
    • 非阻塞
      • offer():如果沒有線程take,直接返回,此方法需要有線程在阻塞take,纔可以獲取到元素

小結

可以看到,此隊列很特殊,其本身並不存放元素,其中的數據結構更像是一個單向鏈表,每一個節點中存放要傳遞的元素,以生產者的角度來看,沒人接收我要放的元素我就會一直等待,這個特性是在阻塞隊列中是比較特殊的。

又因爲其本身並不存儲元素,只是非阻塞的一接一收的特性,使得這種隊列在吞吐量方面會優於以上兩種隊列,所以在高併發低耗時的生產-消費場景下,此隊列的吞吐量表現的比較優秀。

當然,以上分析的是非公平的Transferer實現,讀者也可以去了解一下公平的實現TransferQueue。

此隊列還有一些細節沒有分析到,例如transfer還有第三個分支,幫助分支,線程還會幫助別的線程去喚醒和置換head操作,這是併發大師經常玩的套路,這種思想在ConcurrentHashMap中也存在,多線程在大師手裏可謂是非常靈活,不愧具有高吞吐量、高伸縮性的特性。

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