做者 | 王磊java
來源 | Java中文社羣(ID:javacn666)git
本文已收錄至 https://github.com/vipstone/algorithm 《算法圖解》系列。github
經過前面文章的學習《一文詳解「隊列」,手擼隊列的3種方法!》咱們知道了隊列(Queue)是先進先出(FIFO)的,而且咱們能夠用數組、鏈表還有 List 的方式來實現自定義隊列,那麼本文咱們來系統的學習一下官方是如何實現隊列的。web
Java 中的隊列有不少,例如:ArrayBlockingQueue
、LinkedBlockingQueue
、PriorityQueue
、DelayQueue
、SynchronousQueue
等,那它們的做用是什麼?又是如何分類的呢?算法
其實 Java 中的這些隊列能夠從不一樣的維度進行分類,例如能夠從阻塞和非阻塞進行分類,也能夠從有界和無界進行分類,而本文將從隊列的功能上進行分類,例如:優先隊列、普通隊列、雙端隊列、延遲隊列等。數組

雖然本文的重點是從功能上對隊列進行解讀,但其它分類也是 Java 中的重要概念,因此咱們先來了解一下它們。微信
阻塞隊列和非阻塞隊列
阻塞隊列(Blocking Queue)提供了可阻塞的 put
和 take
方法,它們與可定時的 offer
和 poll
是等價的。若是隊列滿了 put
方法會被阻塞等到有空間可用再將元素插入;若是隊列是空的,那麼 take
方法也會阻塞,直到有元素可用。當隊列永遠不會被充滿時,put
方法和 take
方法就永遠不會阻塞。數據結構

咱們能夠從隊列的名稱中知道此隊列是否爲阻塞隊列,阻塞隊列中包含 BlockingQueue
關鍵字,好比如下這些:app
-
ArrayBlockingQueue -
LinkedBlockingQueue -
PriorityBlockingQueue -
.......
阻塞隊列功能演示
接下來咱們來演示一下當阻塞隊列的容量滿了以後會怎樣,示例代碼以下:編輯器
import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
public class BlockingTest {
public static void main(String[] args) throws InterruptedException {
// 建立一個長度爲 5 的阻塞隊列
ArrayBlockingQueue q1 = new ArrayBlockingQueue(5);
// 新建立一個線程執行入列
new Thread(() -> {
// 循環 10 次
for (int i = 0; i < 10; i++) {
try {
q1.put(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(new Date() + " | ArrayBlockingQueue Size:" + q1.size());
}
System.out.println(new Date() + " | For End.");
}).start();
// 新建立一個線程執行出列
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
// 休眠 1S
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!q1.isEmpty()) {
try {
q1.take(); // 出列
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
以上代碼的執行結果以下:
Mon Oct 19 20:16:12 CST 2020 | ArrayBlockingQueue Size:1
Mon Oct 19 20:16:12 CST 2020 | ArrayBlockingQueue Size:2
Mon Oct 19 20:16:12 CST 2020 | ArrayBlockingQueue Size:3
Mon Oct 19 20:16:12 CST 2020 | ArrayBlockingQueue Size:4
Mon Oct 19 20:16:12 CST 2020 | ArrayBlockingQueue Size:5
Mon Oct 19 20:16:13 CST 2020 | ArrayBlockingQueue Size:5
Mon Oct 19 20:16:14 CST 2020 | ArrayBlockingQueue Size:5
Mon Oct 19 20:16:15 CST 2020 | ArrayBlockingQueue Size:5
Mon Oct 19 20:16:16 CST 2020 | ArrayBlockingQueue Size:5
Mon Oct 19 20:16:17 CST 2020 | ArrayBlockingQueue Size:5
Mon Oct 19 20:16:17 CST 2020 | For End.
從上述結果能夠看出,當 ArrayBlockingQueue
隊列滿了以後就會進入阻塞,當過了 1 秒有元素從隊列中移除以後,纔會將新的元素入列。
非阻塞隊列
非阻塞隊列也就是普通隊列,它的名字中不會包含 BlockingQueue
關鍵字,而且它不會包含 put
和 take
方法,當隊列滿以後若是還有新元素入列會直接返回錯誤,並不會阻塞的等待着添加元素,以下圖所示:
非阻塞隊列的典型表明是 ConcurrentLinkedQueue
和 PriorityQueue
。
有界隊列和無界隊列
有界隊列:是指有固定大小的隊列,好比設定了固定大小的 ArrayBlockingQueue
,又或者大小爲 0 的 SynchronousQueue
。
無界隊列:指的是沒有設置固定大小的隊列,但其實若是沒有設置固定大小也是有默認值的,只不過默認值是 Integer.MAX_VALUE,固然實際的使用中不會有這麼大的容量(超過 Integer.MAX_VALUE),因此從使用者的角度來看至關於 「無界」的。

按功能分類
接下來就是本文的重點了,咱們以功能來劃分一下隊列,它能夠被分爲:普通隊列、優先隊列、雙端隊列、延遲隊列、其餘隊列等,接下來咱們分別來看。
1.普通隊列
普通隊列(Queue)是指實現了先進先出的基本隊列,例如 ArrayBlockingQueue
和 LinkedBlockingQueue
,其中 ArrayBlockingQueue
是用數組實現的普通隊列,以下圖所示:
而 LinkedBlockingQueue
是使用鏈表實現的普通隊列,以下圖所示:

經常使用方法
普通隊列中的經常使用方法有如下這些:
-
offer():添加元素,若是隊列已滿直接返回 false,隊列未滿則直接插入並返回 true; -
poll():刪除並返回隊頭元素,當隊列爲空返回 null; -
add():添加元素,此方法是對 offer 方法的簡單封裝,若是隊列已滿,拋出 IllegalStateException 異常; -
remove():直接刪除隊頭元素; -
put():添加元素,若是隊列已經滿,則會阻塞等待插入; -
take():刪除並返回隊頭元素,當隊列爲空,則會阻塞等待; -
peek():查詢隊頭元素,但不會進行刪除; -
element():對 peek 方法進行簡單封裝,若是隊頭元素存在則取出並不刪除,若是不存在拋出 NoSuchElementException 異常。
注意:通常狀況下 offer() 和 poll() 方法配合使用,put() 和 take() 阻塞方法配合使用,add() 和 remove() 方法會配合使用,程序中經常使用的是 offer() 和 poll() 方法,所以這兩個方法比較友好,不會報錯。
接下來咱們以 LinkedBlockingQueue
爲例,演示一下普通隊列的使用:
import java.util.concurrent.LinkedBlockingQueue;
static class LinkedBlockingQueueTest {
public static void main(String[] args) {
LinkedBlockingQueue queue = new LinkedBlockingQueue();
queue.offer("Hello");
queue.offer("Java");
queue.offer("中文社羣");
while (!queue.isEmpty()) {
System.out.println(queue.poll());
}
}
}
以上代碼的執行結果以下:
Hello
Java
中文社羣
2.雙端隊列
雙端隊列(Deque)是指隊列的頭部和尾部均可以同時入隊和出隊的數據結構,以下圖所示:
接下來咱們來演示一下雙端隊列 LinkedBlockingDeque
的使用:
import java.util.concurrent.LinkedBlockingDeque;
/**
* 雙端隊列示例
*/
static class LinkedBlockingDequeTest {
public static void main(String[] args) {
// 建立一個雙端隊列
LinkedBlockingDeque deque = new LinkedBlockingDeque();
deque.offer("offer"); // 插入首個元素
deque.offerFirst("offerFirst"); // 隊頭插入元素
deque.offerLast("offerLast"); // 隊尾插入元素
while (!deque.isEmpty()) {
// 從頭遍歷打印
System.out.println(deque.poll());
}
}
}
以上代碼的執行結果以下:
offerFirst
offer
offerLast
3.優先隊列
優先隊列(PriorityQueue)是一種特殊的隊列,它並非先進先出的,而是優先級高的元素先出隊。
優先隊列是根據二叉堆實現的,二叉堆的數據結構以下圖所示:
二叉堆分爲兩種類型:一種是最大堆一種是最小堆。以上展現的是最大堆,在最大堆中,任意一個父節點的值都大於等於它左右子節點的值。
由於優先隊列是基於二叉堆實現的,所以它能夠將優先級最好的元素先出隊。
接下來咱們來演示一下優先隊列的使用:
import java.util.PriorityQueue;
public class PriorityQueueTest {
// 自定義的實體類
static class Viper {
private int id; // id
private String name; // 名稱
private int level; // 等級
public Viper(int id, String name, int level) {
this.id = id;
this.name = name;
this.level = level;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
}
public static void main(String[] args) {
PriorityQueue queue = new PriorityQueue(10, new Comparator<Viper>() {
@Override
public int compare(Viper v1, Viper v2) {
// 設置優先級規則(倒序,等級越高權限越大)
return v2.getLevel() - v1.getLevel();
}
});
// 構建實體類
Viper v1 = new Viper(1, "Java", 1);
Viper v2 = new Viper(2, "MySQL", 5);
Viper v3 = new Viper(3, "Redis", 3);
// 入列
queue.offer(v1);
queue.offer(v2);
queue.offer(v3);
while (!queue.isEmpty()) {
// 遍歷名稱
Viper item = (Viper) queue.poll();
System.out.println("Name:" + item.getName() +
" Level:" + item.getLevel());
}
}
}
以上代碼的執行結果以下:
Name:MySQL Level:5
Name:Redis Level:3
Name:Java Level:1
從上述結果能夠看出,優先隊列的出隊是不考慮入隊順序的,它始終遵循的是優先級高的元素先出隊。
4.延遲隊列
延遲隊列(DelayQueue)是基於優先隊列 PriorityQueue
實現的,它能夠看做是一種以時間爲度量單位的優先的隊列,當入隊的元素到達指定的延遲時間以後方可出隊。

咱們來演示一下延遲隊列的使用:
import lombok.Getter;
import lombok.Setter;
import java.text.DateFormat;
import java.util.Date;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class CustomDelayQueue {
// 延遲消息隊列
private static DelayQueue delayQueue = new DelayQueue();
public static void main(String[] args) throws InterruptedException {
producer(); // 調用生產者
consumer(); // 調用消費者
}
// 生產者
public static void producer() {
// 添加消息
delayQueue.put(new MyDelay(1000, "消息1"));
delayQueue.put(new MyDelay(3000, "消息2"));
}
// 消費者
public static void consumer() throws InterruptedException {
System.out.println("開始執行時間:" +
DateFormat.getDateTimeInstance().format(new Date()));
while (!delayQueue.isEmpty()) {
System.out.println(delayQueue.take());
}
System.out.println("結束執行時間:" +
DateFormat.getDateTimeInstance().format(new Date()));
}
static class MyDelay implements Delayed {
// 延遲截止時間(單位:毫秒)
long delayTime = System.currentTimeMillis();
// 藉助 lombok 實現
@Getter
@Setter
private String msg;
/**
* 初始化
* @param delayTime 設置延遲執行時間
* @param msg 執行的消息
*/
public MyDelay(long delayTime, String msg) {
this.delayTime = (this.delayTime + delayTime);
this.msg = msg;
}
// 獲取剩餘時間
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
// 隊列裏元素的排序依據
@Override
public int compareTo(Delayed o) {
if (this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS)) {
return 1;
} else if (this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS)) {
return -1;
} else {
return 0;
}
}
@Override
public String toString() {
return this.msg;
}
}
}
以上代碼的執行結果以下:
開始執行時間:2020-10-20 20:17:28
消息1
消息2
結束執行時間:2020-10-20 20:17:31
從上述結束執行時間和開始執行時間能夠看出,消息 1 和消息 2 都正常實現了延遲執行的功能。
5.其餘隊列
在 Java 的隊列中有一個比較特殊的隊列 SynchronousQueue
,它的特別之處在於它內部沒有容器,每次進行 put()
數據後(添加數據),必須等待另外一個線程拿走數據後才能夠再次添加數據,它的使用示例以下:
import java.util.concurrent.SynchronousQueue;
public class SynchronousQueueTest {
public static void main(String[] args) {
SynchronousQueue queue = new SynchronousQueue();
// 入隊
new Thread(() -> {
for (int i = 0; i < 3; i++) {
try {
System.out.println(new Date() + ",元素入隊");
queue.put("Data " + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 出隊
new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
System.out.println(new Date() + ",元素出隊:" + queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
以上代碼的執行結果以下:
Mon Oct 19 21:00:21 CST 2020,元素入隊
Mon Oct 19 21:00:22 CST 2020,元素出隊:Data 0
Mon Oct 19 21:00:22 CST 2020,元素入隊
Mon Oct 19 21:00:23 CST 2020,元素出隊:Data 1
Mon Oct 19 21:00:23 CST 2020,元素入隊
Mon Oct 19 21:00:24 CST 2020,元素出隊:Data 2
從上述結果能夠看出,當有一個元素入隊以後,只有等到另外一個線程將元素出隊以後,新的元素才能再次入隊。
總結
本文講了 Java 中的 5 種隊列:普通隊列、雙端隊列、優先隊列、延遲隊列、其餘隊列。其中普通隊列的典型表明爲 ArrayBlockingQueue
和 LinkedBlockingQueue
,雙端隊列的表明爲 LinkedBlockingDeque
,優先隊列的表明爲 PriorityQueue
,延遲隊列的表明爲 DelayQueue
,最後還講了內部沒有容器的其餘隊列 SynchronousQueue
。

往期推薦

一文詳解「隊列」,手擼隊列的3種方法!

動圖演示:手擼堆棧的兩種實現方法!

JDK 居然是這樣實現棧的?
本文分享自微信公衆號 - Java中文社羣(javacn666)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。