- 在之前的執行緒池的介紹中我們看到了很多阻塞佇列,這篇文章我們主要來說說阻塞佇列的事,
- 阻塞佇列也就是
BlockingQueue,這個類是一個接 - 口,同時繼承了
Queue介面,這兩個介面都是在JDK5中加入的 , BlockingQueue阻塞佇列是執行緒安全的,在我們業務中是會經常頻繁使用到的,如典型的生產者消費的場景,生產者只需要向佇列中添加,而消費者負責從佇列中獲取,

- 如上圖展示,我們生產者執行緒不斷的
put元素到佇列,而消費者從中take出元素處理,這樣實作了任務與執行任務類之間的解耦,任務都被放入到了阻塞佇列中,這樣生產者和消費者之間就不會直接相互訪問實作了隔離提高了安全性,
并發佇列

- 上面是
Java中佇列Queue類的類圖,我們可以看到它分為兩大類,阻塞佇列與非阻塞佇列 - 阻塞佇列的實作介面是
BlockingQueue而非阻塞佇列的介面是ConcurrentLinkedQueue, 本文主要介紹阻塞佇列,非阻塞佇列不再過多闡述 BlockingQueue主要有下面六個實作類,分別是ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、DelayQueue、PriorityBlockingQueue、LinkedTransferQueue,這些阻塞佇列有著各自的特點和適用場景,后面詳細介紹,- 非阻塞佇列的典型例子如
ConcurrentLinkedQueue, 它不會阻塞執行緒,而是利用了CAS來保證執行緒的安全, - 其實還有一個佇列和
Queue關系很緊密,那就是Deque,這其實是double-ended-queue的縮寫,意思是雙端佇列,它的特點是從頭部和尾部都能添加和洗掉元素,而我們常見的普通佇列Queue則是只能一端進一端出,即FIFO,
阻塞佇列特點
- 阻塞佇列的特點就在于阻塞,它可以阻塞執行緒,讓生產者消費者得以平衡,阻塞佇列中有兩個關鍵方法
Put和Take方法
take方法
take方法的功能是獲取并移除佇列的頭結點,通常在佇列里有資料的時候是可以正常移除的,可是一旦執行take方法的時候,佇列里無資料,則阻塞,直到佇列里有資料,一旦佇列里有資料了,就會立刻解除阻塞狀態,并且取到資料,程序如圖所示:
put方法
put方法插入元素時,如果佇列沒有滿,那就和普通的插入一樣是正常的插入,但是如果佇列已滿,那么就無法繼續插入,則阻塞,直到佇列里有了空閑空間,如果后續佇列有了空閑空間,比如消費者消費了一個元素,那么此時佇列就會解除阻塞狀態,并把需要添加的資料添加到佇列中,程序如圖所示:
是否有界(容量有多大)
- 此外,阻塞佇列還有一個非常重要的屬性,那就是容量的大小,分為有界和無界兩種,
- 無界佇列意味著里面可以容納非常多的元素,例如
LinkedBlockingQueue的上限是Integer.MAX_VALUE,約為 2 的 31 次方,是非常大的一個數,可以近似認為是無限容量,因為我們幾乎無法把這個容量裝滿, - 但是有的阻塞佇列是有界的,例如
ArrayBlockingQueue如果容量滿了,也不會擴容,所以一旦滿了就無法再往里放資料了,
阻塞佇列常見方法
- 首先我們從常用的方法出發,根據各自的特點我們可以大致分為三個大類,如下表所示:
| 分類 | 方法 | 含義 | 特點 |
|---|---|---|---|
| 拋出例外 | add | 添加一個元素 | 如果佇列已滿,添加則拋出 IllegalStateException 例外 |
| remove | 洗掉佇列頭節點 | 當佇列為空后,洗掉則拋出 NoSuchElementException 例外 |
|
| element | 獲取佇列頭元素 | 當佇列為空時,則拋出 NoSuchElementException 例外 |
|
| 回傳無例外 | offer | 添加一個元素 | 當佇列已滿,不會報例外,回傳 false ,如果成功回傳 true |
| poll | 獲取佇列頭節點,并且洗掉它 | 當佇列空時,回傳 Null |
|
| peek | 單純獲取頭節點 | 當佇列為空時反饋 NULL |
|
| 阻塞 | put | 添加一個元素 | 如果佇列已滿則阻塞 |
| take | 回傳并洗掉頭元素 | 如果佇列為空則阻塞 |
- 如上面所示主要的八個方法,相對都比較簡單,下面我們通過實際代碼演示的方式來認識
拋例外型別[add、remove、element]
add
- 向佇列中添加一個元素,如果佇列是有界佇列,當佇列已滿時再添加則拋出例外提示,如下:
BlockingQueue queue = new ArrayBlockingQueue(2); queue.add(1); queue.add(2); queue.add(3);
- 上述代碼中我們創建了一個阻塞佇列容量為2,當我們使用
add向其中添加元素,當添加到第三個時則會拋出例外如下:

remove
remove方法是從佇列中洗掉佇列的頭節點,同時會回傳該元素,當佇列中為空時執行remove方法時則會拋出例外,代碼如下:
private static void groupRemove() { BlockingQueue queue = new ArrayBlockingQueue(2); queue.add("i-code.online"); System.out.println(queue.remove()); System.out.println(queue.remove()); }
- 上述代碼中,我們可以看到,我們想佇列中添加了一個元素
i-code.online, 之后通過remove方法進行洗掉,當執行第二次remove時佇列內已無元素,則拋出例外,如下:

element
element方法是獲取佇列的頭元素,但是并不是洗掉該元素,這也是與remove的區別,當佇列中沒有元素后我們再執行element方法時則會拋出例外,代碼如下:
private static void groupElement() { BlockingQueue queue = new ArrayBlockingQueue(2); queue.add("i-code.online"); System.out.println(queue.element()); System.out.println(queue.element()); } private static void groupElement2() { BlockingQueue queue = new ArrayBlockingQueue(2); System.out.println(queue.element()); }
- 上面兩個方法分別演示了在有元素和無元素的情況
element的使用,在第一個方法中并不會報錯,因為首元素一直存在的,第二個方法中因為空的,所以拋出例外,如下結果:

無例外型別[offer、poll、peek]
offer
offer方法是向佇列中添加元素, 同時反饋成功與失敗,如果失敗則回傳false,當佇列已滿時繼續添加則會失敗,代碼如下:
private static void groupOffer() { BlockingQueue queue = new ArrayBlockingQueue(2); System.out.println(queue.offer("i-code.online")); System.out.println(queue.offer("云棲簡碼")); System.out.println(queue.offer("AnonyStar")); }
- 如上述代碼所示,我們向一個容量為2的佇列中通過
offer添加元素,當添加第三個時,則會反饋false,如下結果:
true true false
poll
poll方法對應上面remove方法,兩者的區別就在于是否會在無元素情況下拋出例外,poll方法在無元素時不會拋出例外而是回傳null,如下代碼:
private static void groupPoll() { BlockingQueue queue = new ArrayBlockingQueue(2); System.out.println(queue.offer("云棲簡碼")); //添加元素 System.out.println(queue.poll()); //取出頭元素并且洗掉 System.out.println(queue.poll()); }
- 上面代碼中我們創建一個容量為2的佇列,并添加一個元素,之后呼叫兩次
poll方法來獲取并洗掉頭節點,發現第二次呼叫時為null,因為佇列中已經為空了,如下:
true 云棲簡碼 null
peek
peek方法與前面的element方法是對應的 ,獲取元素頭節點但不洗掉,與其不同的在于peek方法在空佇列下并不會拋出例外,而是回傳null,如下:
private static void groupPeek() { BlockingQueue queue = new ArrayBlockingQueue(2); System.out.println(queue.offer(1)); System.out.println(queue.peek()); System.out.println(queue.peek()); } private static void groupPeek2() { BlockingQueue queue = new ArrayBlockingQueue(2); System.out.println(queue.peek()); }
- 如上述代碼所示,我么們分別展示了非空佇列與空佇列下
peek的使用,結果如下:

阻塞型別[put、take]
put
put方法是向佇列中添加一個元素,這個方法是阻塞的,也就是說當佇列已經滿的情況下,再put元素時則會阻塞,直到佇列中有空位.
take
take方法是從佇列中獲取頭節點并且將其移除,這也是一個阻塞方法,當佇列中已經沒有元素時,take方法則會進入阻塞狀態,直到佇列中有新的元素進入,
常見的阻塞佇列
ArrayBlockingQueue
ArrayBlockingQueue是一個我們常用的典型的有界佇列,其內部的實作是基于陣列來實作的,我們在創建時需要指定其長度,它的執行緒安全性由ReentrantLock來實作的,
public ArrayBlockingQueue(int capacity) {...}
public ArrayBlockingQueue(int capacity, boolean fair) {...}
- 如上所示,
ArrayBlockingQueue提供的建構式中,我們需要指定佇列的長度,同時我們也可以設定佇列是都是公平的,當我們設定了容量后就不能再修改了,符合陣列的特性,此佇列按照先進先出(FIFO)的原則對元素進行排序, - 和
ReentrantLock一樣,如果ArrayBlockingQueue被設定為非公平的,那么就存在插隊的可能;如果設定為公平的,那么等待了最長時間的執行緒會被優先處理,其他執行緒不允許插隊,不過這樣的公平策略同時會帶來一定的性能損耗,因為非公平的吞吐量通常會高于公平的情況,
LinkedBlockingQueue
- 從它的名字我們可以知道,它是一個由鏈表實作的佇列,這個佇列的長度是
Integer.MAX_VALUE,這個值是非常大的,幾乎無法達到,對此我們可以認為這個佇列基本屬于一個無界佇列(也又認為是有界佇列),此佇列按照先進先出的順序進行排序,
SynchronousQueue
synchronousQueue是一個不存盤任何元素的阻塞佇列,每一個put操作必須等待take操作,否則不能添加元素,同時它也支持公平鎖和非公平鎖,synchronousQueue的容量并不是1,而是0,因為它本身不會持有任何元素,它是直接傳遞的,synchronousQueue會把元素從生產者直接傳遞給消費者,在這個程序中能夠是不需要存盤的- 在我們之前介紹過的執行緒池
CachedThreadPool就是利用了該佇列,Executors.newCachedThreadPool(),因為這個執行緒池它的最大執行緒數是Integer.MAX_VALUE,它是更具需求來創建執行緒,所有的執行緒都是臨時執行緒,使用完后空閑60秒則被回收,
PriorityBlockingQueue
PriorityBlockingQueue是一個支持優先級排序的無界阻塞佇列,可以通過自定義實作compareTo()方法來指定元素的排序規則,或者通過構造器引數Comparator來指定排序規則,但是需要注意插入佇列的物件必須是可比較大小的,也就是Comparable的,否則會拋出ClassCastException例外,- 它的
take方法在佇列為空的時候會阻塞,但是正因為它是無界佇列,而且會自動擴容,所以它的佇列永遠不會滿,所以它的put方法永遠不會阻塞,添加操作始終都會成功
DelayQueue
DelayQueue是一個實作PriorityBlockingQueue的延遲獲取的無界佇列,具有“延遲”的功能,DelayQueue應用場景:1. 快取系統的設計:可以用DelayQueue保存快取元素的有效期,使用一個執行緒回圈查詢DelayQueue,一旦能從DelayQueue中獲取元素時,表示快取有效期到了,2. 定時任務調度,使用DelayQueue保存當天將會執行的任務和執行時間,一旦從DelayQueue中獲取到任務就開始執行,從比如TimerQueue就是使用DelayQueue實作的,- 它是無界佇列,放入的元素必須實作
Delayed介面,而Delayed介面又繼承了Comparable介面,所以自然就擁有了比較和排序的能力,代碼如下:
public interface Delayed extends Comparable<Delayed> { long getDelay(TimeUnit unit); }
- 可以看出
Delayed介面繼承Comparable,里面有一個需要實作的方法,就是getDelay,這里的getDelay方法回傳的是“還剩下多長的延遲時間才會被執行”,如果回傳 0 或者負數則代表任務已過期, - 元素會根據延遲時間的長短被放到佇列的不同位置,越靠近佇列頭代表越早過期,
有完整的Java初級,高級對應的學習路線和資料!專注于java開發,分享java基礎、原理性知識、JavaWeb實戰、spring全家桶、設計模式、分布式及面試資料、開源專案,助力開發者成長!
歡迎關注微信公眾號:碼邦主

作者:AnonyStar
鏈接:https://my.oschina.net/u/3767760/blog/4718879
來源:oschina
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/245507.html
標籤:Java
