Java中的Queue之概述

對技術還是得有敬畏之心,總覺得Queue好像沒啥,其實只是沒有仔細去了解過。

不過自從上次認真地看了線程池的源代碼之後,發現Queue是一個很神奇的集合類。Queue的形式有無界、有界,還有堵塞、非堵塞。初略想想,這個實現可能就不簡單。

一個問題

在線程池中,自定義線程池時,放入什麼樣的隊列可以讓線程數達不到maximumPoolSize

按照線程池的源碼,線程增長有兩個階段:一個階段是在達到corePoolSize之前,第二個階段是在達到corePoolSize之後並且阻塞隊列已經滿了,纔會繼續增加線程,直到maximumPoolSize

所以解決方案有如下兩種:

①如果隊列是無界的,那麼將不會到達第二階段的增長,也就無法到達maximumPoolSize

②還有可以讓corePoolSize在足夠大的值,同時限定堆大小,保證在有限的任務數下,引發內存不足也算是一種解決方案。

在解決方案①的基礎上,我給出了LinkedBlockingQueue這個答案,但是它是一個“有界”的堵塞隊列,只是它的capacity默認是Integer.MAX_VALUE。因爲一開始我得知它是一個無界隊列,因爲未做詳細、深入的瞭解,但是後面我被指正後,引發了我對Queue的思考,因爲不常用,所以瞭解的確不是很多;同時,Queue不僅僅是一個FIFO,還有同步操作在裏面,就是***生產者-消費者***的同步問題的一個解決方案。綜上,這讓我對堵塞隊列產生了強烈的興趣。

概覽

找了一下BlockingQueue大致的實現類,它們的類圖如下。

從中可以看出,它們都繼承自AbstractQueue,並且都實現了BlockingQueue接口,只有LinkedTransferQueue還實現了TransferQueue。加上對Queue的一些操作並不是特別熟悉,比如offer、peek、poll之類,所以從Queue接口開始,逐個瞭解其大致的功能。

Queue

這個接口定義了6個方法,功能分成3個,分別是插入刪除獲取隊首元素,每個功能有2種實現,對應如下表中的關係。這兩種實現的區別也很明顯

Throws exception Returns special value
Insert add(e) offer(e)
Remove remove() poll()
Examine element() peek()
  • add(e)offer(e):添加失敗時,add拋異常,offer返回false。
  • remove()poll() :沒有元素時,remove拋異常,poll返回null。
  • element()peek():沒有元素時,remove拋異常,peek返回null。

還有一點需要注意,雖然有的Queue的實現(LinkedList)允許插入null值,但是Queue通常不允許插入null值,因爲null對peek、poll來說,是queue中沒有元素。

BlockingQueue

繼承自Queue接口,同時在Queue的基礎上,新增了堵塞插入、堵塞獲取並刪除、可堵塞一定時長的插入、可堵塞一定時長的獲取並刪除。相應關係對應下表。

Throws exception Special value Blocks Times out
Insert add(e) offer(e) put(e) offer(e, time, unit)
Remove remove() poll() take() poll(time, unit)
Examine element() peek() not applicable not applicable

BlockingQueue設計主要被用來處理***生產者-消費者***問題。還有一點需要注意BlockingQueue不接受null元素。插入操作***happen-before***讀取、移除。

AbstractQueue

抽象類,繼承自AbstractCollection,並實現了Queue接口,但未給出具體實現,相當於引入Queue相應方法,在自身類中使用,然後靠子類實現Queue的相關方法。

它實現的add、remove、element方法,分別依靠offer、poll、peek,具體如下:

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

public E remove() {
    E x = poll();
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}

public E element() {
    E x = peek();
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}

public void clear() {
    while (poll() != null)
        ;
}

正好對應於上面Queue方法的區別。

常見實現類

Java中線程安全的內置隊列如下表所示。隊列的底層一般分成三種:數組鏈表。其中,堆一般情況下是爲了實現帶有優先級特性的隊列。

隊列 有界性 數據結構
ArrayBlockingQueue bounded 加鎖 arraylist
LinkedBlockingQueue optionally-bounded 加鎖 linkedlist
ConcurrentLinkedQueue unbounded 無鎖 linkedlist
LinkedTransferQueue unbounded 無鎖 linkedlist
PriorityBlockingQueue unbounded 加鎖 heap
DelayQueue unbounded 加鎖 heap

基於數組線程安全的隊列,比較典型的是ArrayBlockingQueue,它主要通過加鎖的方式來保證線程安全;

基於鏈表的線程安全隊列,分成LinkedBlockingQueueConcurrentLinkedQueue兩大類,前者也通過鎖的方式來實現線程安全,而後者以及上面表格中的LinkedTransferQueue都是通過原子變量CAS這種不加鎖的方式來實現的。

通過不加鎖的方式實現的隊列都是無界的(無法保證隊列的長度在確定的範圍內);而加鎖的方式,可以實現有界隊列

在穩定性要求特別高的系統中,爲了防止生產者速度過快,導致內存溢出,只能選擇有界隊列;同時,爲了減少Java的垃圾回收對系統性能的影響,會盡量選擇array/heap格式的數據結構。

Reference:

https://tech.meituan.com/2016/11/18/disruptor.html

https://www.cnblogs.com/WangHaiMing/p/8798709.html

https://www.cnblogs.com/stateis0/p/9062076.html

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