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

1、概述

如果要將java.util.concurrent工具包中的各種工具類進行詳細的功能分類,那麼在這個工具包中可以將“隊列”性質的工具類專門作爲一個獨立的功能分類。爲了適應高併發的程序工作場景,java.util.concurrent工具提供了豐富用於高併發場景的,線程安全的Queue/Deque結構集合,整體類結構可由下圖進行描述:
在這裏插入圖片描述
在上文中我們已經介紹了隊列的基本工作特點:允許在隊列的head(頭)位置取出元素,並且只允許在隊列的tail(尾)位置添加元素,也就是說先進入隊列的元素會先從隊列取出(先進先出FIFO)。除此之外,如上圖所示的這些隊列都有一些自身的工作特點:

  • ArrayBlockingQueue:這是一種內部基於數組的,使用在高併發場景下的阻塞隊列,也是一種容量有界的隊列。這個隊列最顯著的工作特性是,存儲在隊列中的總元素數量有一個最大值,這個隊列的使用場景也非常豐富,後文將進行詳細講解。

  • LinkedBlockingQueue:這是一種內部基於鏈表的,使用在高併發場景下的阻塞隊列,是一種容量無界的隊列。這個隊列最顯著的工作特點就是他的內部結構是一個鏈表,這保證了它可以在有界隊列和無界隊列間非常方便的進行轉換。

  • ConcurrentLinkedQueue:和LinkedBlockingQueue相比,這也是一種內部基於鏈表的,可以在有更高性能要求的場景下使用的容量無界的,體現先進先出工作特點的隊列。但它不是一種阻塞隊列,其內部主要也是使用CAS思想進行實現,通過我們對java parking鎖機制分析的相關內容可以知道,CAS思想在大多數情況下比基於parking鎖機制實現的工具類,工作性能要高(但也不是絕對的)。

  • LinkedTransferQueue:這是一個基於“鏈表”的,可以在高併發場景下使用的阻塞隊列,它是一種無界隊列。可以將它看成LinkedBlockingQueue和ConcurrentLinkedQueue兩者優點的結合體,既關注集合的讀寫性能,又維持隊列集合的工作特性。

  • PriorityBlockingQueue:這是一種內部基於數組的,採用小頂堆結構的,可以在高併發場景下使用的阻塞隊列,它也是一種容量無界的隊列。這個隊列最顯著的工作特點是,隊列集合中的元素將按照堆樹結構進行排序,以保證從該隊列取出的元素都是集合中權值最小的元素。

  • DelayQueue:這是一種內部依賴PriorityQueue的,採用小頂堆結構的,可以在高併發場景下使用的阻塞隊列,它也是一種容量無界的隊列。這個隊列最顯著的工作特點是,隊列集合中的元素除了將按照堆樹結構進行排序外,這些元素還通過實現java.util.concurrent.Delayed接口,定義一個延遲時間,只有當權值最小的元素的延遲時間小於等於0時,該元素纔會被外部調用者獲取到(這是一個實現租約協議的很好的基礎思想,不過還差了一些要點,本專題後續文章會進行說明)。

  • SynchronousQueue:這是一個內部“只能存儲”一個元素的阻塞隊列(基於),很明顯它是一個有界隊列。這個隊列最顯著的工作特點是,一個調用者向該隊列放入一個元素後,就會進入阻塞狀態,直到另一個調用者將隊列中的這個元素取出。同樣來說,如果一個調用者需要從該隊列中取出一個元素,但隊列中有沒有元素,那麼該調用者也會進去阻塞狀態,直到另一個調用者向該隊列放入一個元素位置——總結來說,就是向隊列放入元素和取出元素的調用者要成對出現。

2、Queue/Deque接口和BlockQueue/BlockingDeque接口

這裏我們對什麼叫阻塞隊列什麼叫非阻塞隊列,什麼叫有界隊列什麼叫無界隊列進行說明。

2.1、什麼是有界隊列,什麼是無界隊列

首先有界隊列和無界隊列的說法是在多線程、高併發場景下對隊列容量特點的描述。

  • 有界隊列:即隊列容量有一個固定大小的容量上限,一旦隊列中的元素總量達到最大容量時,隊列就會對添加操作做容錯性處理,如返回false證明操作失敗、拋出運行時異常、進入阻塞狀態直到操作條件滿足要求等——一句話,不再允許元素被添加了。

  • 無界隊列:即隊列容量可以沒有一個固定大小的容量上限,或者容量上限是一個很大的理論上限值(例如常量Integer.MAX_VALUE,就大值爲2147483647)。由於這種隊列理論上沒有容量上限,所以理論上調用者可以將任意多的元素添加到集合中,而不會引起添加操作出現容量異常。

這裏要注意,無界隊列是不是真的無界呢?顯然不是的,首先通過上文的定義我們可知一部分無界隊列是可以在隊列實例化時設置其隊列容量上限的,例如LinkedBlockingQueue這個隊列默認的容量大小是Integer.MAX_VALUE(相當於無界),但是我們也可以設定LinkedBlockingQueue隊列容量爲一個特定的值。

其次,既然無界隊列可以不設定固定大小的容量上限,換句話說就是無界隊列也可以設定固定大小的容量上限,例如以上例舉的LinkedBlockingQueue隊列,就可以設定一個容量上限,從而變成有界隊列。

最後,無界隊列也不可能保證其容量無限大,因爲JVM可申請的堆內存是有上限的,當超過堆內存容量且JVM無法再申請新的內存空間時,應用程序就會拋出OutofMemoryError異常。

2.2、什麼是阻塞隊列,什麼是非阻塞隊列

Queue接口是BlockQueue接口的父級接口,前者定義了一些和隊列相關的接口,後者在此基礎上又補充了另一些接口功能,Queue接口主要的功能方法如下所示:

// 以下是java.util.Queue接口的主要定義
public interface Queue<E> extends Collection<E> {
  // 這是一種添加操作,如果不違反隊列集合的容量限制要求,則立即向集合添加新的元素,並返回true
  // 其它情況則拋出IllegalStateException異常,添加失敗
  boolean add(E e);
  
  // 這也是一種添加操作,如果不違反隊列集合的容量限制要求,則立即向集合添加新的元素,並返回true
  // 其它添加失敗的情況,則返回false(不是拋出異常)
  // add方法和offer方法,在添加操作的邊界限制中,也有一些共同的限制,例如如果添加的元素是null,則都會拋出NullPointerException運行時異常
  // 再例如,如果添加的元素類型不符合要求,則會拋出ClassCastException運行時異常
  boolean offer(E e);
  
  // 這是一種移除操作,該操作將從隊列集合頭部移除操作,移除的元素將被返回給調用者
  // 如果操作時,隊列集合中沒有元素,則拋出NoSuchElementException異常
  E remove();

  // 這也是一種移除操作,該操作將從隊列集合頭部移除操作,移除的元素將被返回給調用者
  // 如果操作時,隊列集合中沒有元素,則返回null。
  E poll();
  
  // 這是一種查詢操作,該操作將查詢當前隊列集合頭部的元素(但不會移除),並進行返回
  // 如果操作時,隊列集合中沒有元素,則拋出NoSuchElementException異常
  E element();
  
  // 這也是一種查詢操作,該操作將查詢當前隊列集合頭部的元素(但不會移除),並進行返回
  // 如果操作時,隊列集合中沒有元素,則返回null。
  E peek();
}

通過以上源代碼,我們可以知道,java.util.Queue接口中主要定義了六個方法,這六個方法可以分爲兩類:一類是在操作時如果隊列集合的容量狀態不符合要求,就會拋出異常;另一類是在操作時如果集合的狀態不符合要求,則儘可能不拋出異常——使用返回一些特定的值進行替換。通過下表我們可以對這6個方法進行詳細分類

操作類型 拋出異常的操作 返回特定值的操作(null或者false)
添加操作 add offer
刪除操作 remove poll
查詢操作 element peek

這裏要特別說明的情況是,拋出異常的方法在拋出諸如NoSuchElementException、IllegalStateException等異常時,其關注的點只是隊列集合的容量狀態,而其它異常的拋出要求是沒有區別的。所以在閱讀源代碼時會出現的有趣情況是,那些無界隊列的實現中,往往可以看到其add方法直接調用了offer方法;那些有界隊列中才對offer方法返回false的情況進行了異常拋出處理。

例如我們後文將要詳細介紹的ConcurrentLinkedQueue非阻塞隊列,其add方法就是直接調用了offer方法——原因很簡單,因爲無界隊列本來就對容器的上限容量沒有限制,所以也就不存在添加操作會超界的場景,如下所示:

public class ConcurrentLinkedQueue<E> extends AbstractQueue<E>
        implements Queue<E>, java.io.Serializable {
  // ......
  public boolean add(E e) {
    return offer(e);
  }
  // ......
}

堆內存不足的情況不在這裏考慮,堆內存不足的情況自然會由JVM報告OutOfMemoryError。

再例如我們後文將要詳細介紹的有界隊列ArrayBlockingQueue,其內部的add方法就會offer方法返回值進行了特別判定,如下源代碼段落所示:

public abstract class AbstractQueue<E> extends AbstractCollection<E> implements Queue<E> {
  // ......
  public boolean add(E e) {
    if (offer(e)) {
      return true;
    }
    else {
      throw new IllegalStateException("Queue full");
    }
  }
  // ......
}

public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
  // ......
  public boolean add(E e) {
    return super.add(e);
  }
  // ......
}

另外,我們可以發現java.util.Queue接口中主要定義了六個方法都是“實時”處理,也就是說調用者對這6個方法的調用都不會引起調用者所在線程的阻塞——無論調用者是向集合進行添加操作還是移除操作,又或者查詢操作。這種工作特性的隊列稱爲非阻塞隊列。

BlockQueue接口中定義的一些方法和Queue接口中定義的部分方法重複,但是還新增了一些方法,那些兩個接口重複的方法這裏就不再進行贅述了,我們主要來分析一下那些在BlockQueue接口中新增加的方法定義,如下所示:

public interface BlockingQueue<E> extends Queue<E> {
  // 該方法將在有界隊列的場景下,試圖在給定的限制時間內,將一個元素添加到隊列。
  // 如果超過限制時間前,仍然沒有操作成功,則調用者所在線程會進入阻塞狀態。
  // 如果操作成功,將返回true;其它情況返回false(例如超過限制時間)
  boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
  // 該方法將試圖從隊列頭部取出元素,如果沒有可以取得的元素,則調用者所在線程進入阻塞狀態
  E take() throws InterruptedException;
  // 該方法試圖在給定的限制時間內,將隊列頭的元素進行移除。
  // 如果超過限制時間前,仍然沒有操作成功,則調用者所在線程會進入阻塞狀態。
  // 如果操作成功,則返回被移除的元素;其它情況返回null(例如超過限制時間,隊列中仍然沒有任何可以移除的元素)
  E poll(long timeout, TimeUnit unit) throws InterruptedException;
  // 該方法從隊列集合中移除指定的元素
  boolean remove(Object o);
  // 該方法從隊列集合中“移動”所有元素到指定集合中,“移動”操作的意義在於這些元素將從原來的隊列集合中刪除
  // 注意:如果指定額元素移除集合和移入集合是同一個集合,則會拋出IllegalArgumentException異常。
  int drainTo(Collection<? super E> c);
} 

這裏請注意offer(E e, long timeout, TimeUnit unit)方法,該方法僅限於有界隊列的場景,也就是說如果是無界隊列則該方法的工作意義和offer(E e)方法的工作意義相同,所以讀者可以發現無界隊列的源代碼實現中,這兩個方法的實現邏輯相同。如下是PriorityBlockingQueue隊列中,兩個方法的實現:

public class PriorityBlockingQueue<E> extends AbstractQueue<E> 
  implements BlockingQueue<E>, java.io.Serializable {
  // ......
  public boolean offer(E e) {
    // ......
  }
  
  // 該方法直接調用offer(E e)方法
  public boolean offer(E e, long timeout, TimeUnit unit) {
    return offer(e); // never need to block
  }
  // ......
}

由此,我們可以給阻塞隊列一個通俗的定義,即實現了java.util.concurrent.BlockingQueue接口的隊列中有一組方法功能,當調用者通過這組方法對隊列進行的讀寫操作不滿足操作條件時,調用者所在線程將進入阻塞狀態,直到操作條件被滿足或者超過限制時間。

3、後文說明

實現了java.util.concurrent.BlockingQueue接口的隊列是java線程安全的集合體系中非常重要的一組集合類型,後續文章我們將花費較多的篇幅對這些具體實現類進行詳細介紹,包括但不限於:ArrayBlockingQueue、LinkedBlockingQueue、DelayQueue、PriorityBlockingQueue、LinkedTransferQueue、SynchronousQueue。

(接下文《源碼閱讀(31):Java中線程安全的Queue、Deque結構——ArrayBlockingQueue阻塞隊列》)

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