Java BlockingQueue 阻塞式隊列

常用 BlockingQueue:ArrayBlockingQueue、LinkedBlockingQueue、DelayQueue、PriorityBlockingQueue、SynchronousQueue 

2014拍攝於四川羌族藏族自治區郎木寺。

微信公衆號

 

王皓的GitHub:https://github.com/TenaciousDWang

 

今天這一回,我們來說一下Java併發編程中的阻塞隊列BlockingQueue。

 

阻塞隊列BlockingQueue很好的解決了多線程中,如何高效安全“傳輸”數據的問題。通過這些高效並且線程安全的隊列類,爲我們快速搭建高質量的多線程程序帶來極大的便利。

 

首先我們現在看一下什麼是隊列,以下是一個隊列模型示例。

 

 

我們可以看到數據由隊列的一端輸入,從另外一端輸出,常用的隊列主要有以下兩種:

 

1、先進先出(FIFO):在之前我們說AQS時,提出過FIFO這個概念,先插入的隊列的元素也最先出隊列,類似於排隊的功能。從某種程度上來說這種隊列也體現了一種公平性。

 

2、後進先出(LIFO):後插入隊列的元素最先出隊列,這種隊列優先處理最近發生的事件。 

 

我們再來看一下阻塞隊列,我們用一個經典的生產者與消費者的例子來說明,假設有若干生產線程,若干消費線程,生產者線程生產數據供給消費者來消費,如果在一定時間內,生產者的速度大於消費者的速度,那麼就會產生過量的數據,這些數據需要被緩存起來,且當緩存裝滿時,生產者線程需要被掛起暫時停止生產,等待消費者線程將緩存裏的數據取走。反之,生產者線程速度跟不上消費者速度則消費者線程應該掛起,等待生產者線程生產數據。

 

在JUC包沒有發佈之前,要解決這個問題就必須額外地實現同步策略以及線程間喚醒策略,這個實現起來就非常麻煩,所有的程序員都必須去自己控制這些細節,尤其還要兼顧效率和線程安全。

 

JUC包發佈之後,爲我們帶來了BlockingQueue阻塞隊列,上面的生產者與消費者例子裏,比如一個線程從一個空的阻塞隊列中取元素,此時線程會被阻塞直到阻塞隊列中有了元素。當隊列中有元素後,被阻塞的線程會自動被喚醒(不需要我們編寫代碼去喚醒)。這樣提供了極大的方便性。

 

根據在生產者和消費者裏使用阻塞隊列我們來用圖片展示一下。

 

 

1、當隊列中沒有數據的情況下,消費者所有線程都會被自動阻塞,直到有數據放入隊列。

 

 

2、當隊列中填滿數據的情況下,生產者端的所有線程都會被自動阻塞,直到隊列中有空的位置,生產線程被自動喚醒。

 

 

我們先來看一下BlockingQueue中的核心方法,首先是插入數據:

 

1、offer(E e)表示如果可能的話,將e加到BlockingQueue裏,即如果BlockingQueue可以容納,則返回true,否則返回false.

 

2、put(E e)把e加到BlockingQueue裏,如果BlockQueue沒有空間,則調用此方法的線程被阻斷直到BlockingQueue裏面有空間再繼續.

       

3、offer(E e, long timeout, TimeUnit unit)可以設定等待的時間,如果在指定的時間內,還不能往隊列中加入,則返回失敗。

 

接下來是獲取數據:

 

1、poll()這個其實是BlockingQueue繼承Queue的方法,取走BlockingQueue裏排在首位的對象,如果隊列是空的返回null。

 

2、poll(long timeout, TimeUnit unit)從BlockingQueue取出一個隊首的對象,如果在指定時間內,隊列一旦有數據可取,則立即返回隊列中的數據。否則知道時間超時還沒有數據可取,返回失敗。

 

3、take()檢索並刪除此隊列的頭,如有必要則等待,直到某個元素可用爲止。

 

4、int drainTo(Collection<? super E> c)一次性從BlockingQueue獲取所有可用的數據對象(int drainTo(Collection<? super E> c, int maxElements)還可以指定獲取數據的個數),通過該方法,可以提升獲取數據效率,不需要多次分批加鎖或釋放鎖。

 

接下來我們來看一下用的多的幾個主要阻塞隊列,並用代碼示例來說明如何使用。

 

1、ArrayBlockingQueue

 

基於數組的阻塞隊列實現,在ArrayBlockingQueue內部,維護了一個定長數組,以便緩存隊列中的數據對象,這是一個常用的阻塞隊列,除了一個定長數組外,ArrayBlockingQueue內部還保存着兩個整形變量,分別標識着隊列的頭部和尾部在數組中的位置。

 

 

ArrayBlockingQueue是一個由數組結構組成的有界阻塞隊列,我可以從構造方法看到。必須指定數組大小,所有爲有界。並且可以指定公平性與非公平性,默認情況下爲非公平的,即不保證等待時間最長的隊列最優先能夠訪問隊列。第三個爲可以指定在初始化時加入一個集合,基本沒用過。 

 

ArrayBlockingQueue在生產者放入數據和消費者獲取數據,都是共用同一個鎖對象,生產者與消費者無法並行。

 

我們來寫一個例子,首先創建一個生產者。

 

 

然後創建一個消費者。

 

 

最後創建一個測試類,來啓動兩個線程即可。

 

 

2.LinkedBlockingQueue

 

內部也維持着一個鏈表構成的數據緩衝隊列LinkedBlockingQueue不同於ArrayBlockingQueue,它如果不指定容量,默認爲Integer.MAX_VALUE,在這裏,我們需要注意的是:如果構造一個LinkedBlockingQueue對象,而沒有指定其大小,LinkedBlockingQueue會默認爲Integer.MAX_VALUE,這樣的話,如果生產者的速度大於消費者的速度,也許還沒有等隊列阻塞產生,系統內存就已經被消耗殆盡了。 

 

同ArrayBlockingQueue不同,生產者端和消費者端分別採用了獨立的鎖來控制數據同步,內部由兩個ReentrantLock來實現出入隊列的線程安全,由各自的Condition對象的await和signal來實現等待和喚醒功能。這也意味着在高併發的操作下生產者和消費者可以並行的操作隊列中的數據,依次來提高整個隊列的併發性能。

 

ArrayBlockingQueue採用的是數組作爲數據存儲容器,而LinkedBlockingQueue採用的則是以Node節點作爲連接對象的鏈表。 前者在插入或刪除元素時不會產生或銷燬任何額外的對象實例,而後者則會生成一個額外的Node對象。

 

來看一下LinkedBlockingQueue的put方法。

 

 

來看一下LinkedBlockingQueue的take方法。

 

 

最後我們來舉一個例演示一下LinkedBlockingQueue的使用方法。

 

 

3.DelayQueue

 

DelayQueue中的元素只有當其指定的延遲時間到了,才能夠從隊列中獲取到該元素。DelayQueue是一個沒有大小限制的隊列,是一個無界的BlockingQueue。

 

因此往隊列中插入數據的操作(生產者)永遠不會被阻塞,而只有獲取數據的操作(消費者)纔會被阻塞。

 

DelayQueue用於放置實現了Delayed接口的對象,其中的對象只能在其到期時才能從隊列中取走。這種隊列是有序的,即隊頭對象的延遲到期時間最長。注意:不能將null元素放置到這種隊列中。

 

Delayed,一種混合風格的接口,用來標記那些應該在給定延遲時間之後執行的對象。此接口的實現必須定義一個 compareTo 方法,該方法提供與此接口的 getDelay 方法一致的排序。

 


首先創建一個消息對象繼承Delayed,並覆寫compareTo與getDelay這兩個方法。

 

 

然後我們創建一個消息消費者。

 

 

最後我們來創建一個延時隊列,並添加消息,然後執行消費者線程。

 

 

添加兩個消息體,id號實際決定運行順序,1號消息在線程啓動後1秒後打印,2號消息在1號打印2秒後打印,總共延時爲三秒。

 

我們來看一下打印結果。

 

 

如果將id號顛倒,則三秒後打印world,並馬上打印hello,由於hello被阻塞在world後面,且延時時間已到。

 

4. PriorityBlockingQueue

 

   基於優先級的阻塞隊列(優先級的判斷通過構造函數傳入的Compator對象來決定),但需要注意的是PriorityBlockingQueue並不會阻塞數據生產者,而只會在沒有可消費的數據時,阻塞數據的消費者。因此使用的時候要特別注意,生產者生產數據的速度絕對不能快於消費者消費數據的速度,否則時間一長,會最終耗盡所有的可用堆內存空間。在實現PriorityBlockingQueue時,內部控制線程同步的鎖採用的是公平鎖。由上可知是一個無界有序的阻塞隊列。

 

        一般使用優先級的隊列的場景最常見的就是VIP排隊,誰的VIP等級高,誰就排在前面,哪怕是後來進入隊列的,這裏我們來舉一個例子演示一下優先級隊列的使用。

 

首先創建一個Customer類,它包括姓名和VIP等級兩個屬性。

 

 

然後寫一個比較客戶VIP等級的類VipComparator

 

 

接下來,添加一個生產者隊列隨機生成客戶放入優先級隊列中。

 

再添加一個消費者隊列,用來處理客戶的排隊請求。

 

 

最後我們創建一個測試類,在隊列中添加10條數據,使用join完成後,開始執行消費者線程處理隊列中的內容,看一下是否按照VIP等級順序執行。

 

 

看一下打印結果:

 

 

VIP等級高的優先執行。

 

5、SynchronousQueue

 

一種無緩衝的等待隊列,類似於無中介的直接交易,有點像原始社會中的生產者和消費者,生產者拿着產品去集市銷售給產品的最終消費者,而消費者必須親自去集市找到所要商品的直接生產者,如果一方沒有找到合適的目標,那麼對不起,大家都在集市等待。相對於有緩衝的BlockingQueue來說,少了一箇中間經銷商的環節(緩衝區),如果有經銷商,生產者直接把產品批發給經銷商,而無需在意經銷商最終會將這些產品賣給那些消費者,由於經銷商可以庫存一部分商品,因此相對於直接交易模式,總體來說採用中間經銷商的模式會吞吐量高一些(可以批量買賣);但另一方面,又因爲經銷商的引入,使得產品從生產者到消費者中間增加了額外的交易環節,單個產品的及時響應性能可能會降低。

 

聲明一個SynchronousQueue有兩種不同的方式,它們之間有着不太一樣的行爲。公平模式和非公平模式的區別:

 

1、如果採用公平模式:SynchronousQueue會採用公平鎖,並配合一個FIFO隊列來阻塞多餘的生產者和消費者,從而體系整體的公平策略。

 

2、但如果是非公平模式(SynchronousQueue默認):SynchronousQueue採用非公平鎖,同時配合一個LIFO隊列來管理多餘的生產者和消費者,而後一種模式,如果生產者和消費者的處理速度有差距,則很容易出現飢渴的情況,即可能有某些生產者或者是消費者的數據永遠都得不到處理。

 

這裏寫一個Demo來一下如何使用:

 

 

輸出結果爲:

 

 

SynchronousQueue 內部沒有容量,但是由於一個插入操作總是對應一個移除操作,反過來同樣需要滿足。那麼一個元素就不會再SynchronousQueue 裏面長時間停留,一旦有了插入線程和移除線程,元素很快就從插入線程移交給移除線程。也就是說這更像是一種信道(管道),資源從一個方向快速傳遞到另一方 向。顯然這是一種快速傳遞元素的方式,也就是說在這種情況下元素總是以最快的方式從插入着(生產者)傳遞給移除着(消費者),這在多任務隊列中是最快處理任務的方式。在線程池裏的一個典型應用是Executors.newCachedThreadPool()就使用了SynchronousQueue,這個線程池根據需要(新任務到來時)創建新的線程,如果有空閒線程則會重複使用,線程空閒了60秒後會被回收。

 

以上就是常用的幾種阻塞隊列的使用展示及介紹。

 

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