什麼是阻塞隊列(BlockingQueue)?
文章目錄
項目環境
- jdk 1.8
- github 地址:https://github.com/huajiexiewenfeng/java-concurrent
- 本章模塊:blockingqueue
1.阻塞隊列(BlockingQueue)
BlockingQueue 繼承了 Queue 接口,是隊列的一種。Queue 和 BlockingQueue 都是在 Java 5 中加入的。
- java.util.concurrent.BlockingQueue
public interface BlockingQueue<E> extends Queue<E> {...}
BlockingQueue 有 6 種最主要的實現如下(《常見的阻塞隊列有哪些?》會專門討論這幾種阻塞隊列):
- ArrayBlockingQueue
- LinkedBlockingQueue
- SynchronousQueue
- DelayQueue
- PriorityBlockingQueue
- LinkedTransferQueue
還有一個和 Queue 關係緊密的 Deque 接口,它繼承了 Queue,如代碼所示:
public interface Deque<E> extends Queue<E> {...}
Deque 的意思是雙端隊列,是 double-ended-queue 的縮寫,它從頭和尾都能添加和刪除元素;而普通的 Queue 只能從一端進入,另一端出去。這是 Deque 和 Queue 的不同之處,Deque 其他方面的性質都和 Queue 類似。
2.生產者消費者模式
阻塞隊列的典型使用場景就是 生產者/消費者模式
我們先利用 ArrayBlockingQueue 來實現一個多線程版本的生產者/消費者模式,如圖:
- 阻塞隊列的大小爲 3
- 使用三個生產者線程添加數據
- 使用三個消費者線程消費數據
實現代碼:
public class BlockingQueueDemo {
static ArrayBlockingQueue<String> abq = new ArrayBlockingQueue(3);
public static void main(String[] args) {
// 生產者
for (int i = 0; i < 3; i++) {
new Thread(() -> producer(), "producerThread" + i).start();
}
// 消費者
for (int i = 0; i < 3; i++) {
new Thread(() -> consumer(), "consumerThread" + i).start();
}
}
private static void consumer() {
while (true) {
try {
String msg = abq.take();
System.out.println(Thread.currentThread().getName() + " ->receive msg:" + msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static void producer() {
for (int i = 0; i < 100; i++) {
try {
abq.put("[" + i + "]");
System.out.println(Thread.currentThread().getName() + " ->send msg:" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
執行結果:
producerThread1 ->send msg:0
producerThread2 ->send msg:0
producerThread0 ->send msg:0
consumerThread1 ->receive msg:[0]
producerThread1 ->send msg:1
consumerThread2 ->receive msg:[0]
producerThread1 ->send msg:2
producerThread2 ->send msg:1
consumerThread1 ->receive msg:[0]
consumerThread0 ->receive msg:[1]
...
3.阻塞隊列的優點
從上面的圖以及示例代碼分析,我們可以看到阻塞隊列的幾個優點
3.1 降低多線程開發的難度
由於阻塞隊列本身是線程安全的,隊列可以安全地從一個線程向另外一個線程傳遞數據,所以我們的生產者/消費者直接使用線程安全的隊列就可以,而不需要自己去考慮更多的線程安全問題。這也就意味着,考慮鎖等線程安全問題的重任從 你
轉移到了 隊列
上,降低了我們開發的難度和工作量。
3.2 隔離代碼,實現業務代碼解耦
隊列還能起到一個隔離的作用。比如說我們開發一個銀行轉賬的程序,那麼生產者線程不需要關心具體的轉賬邏輯,只需要把轉賬任務,如賬戶和金額等信息放到(put)隊列中就可以,而不需要去關心銀行這個類如何實現具體的轉賬業務。
而作爲銀行這個類來講,它會去從隊列裏(take)取出來將要執行的具體的任務,再去通過自己的各種方法來完成本次轉賬。
這樣就實現了具體任務與執行任務類之間的解耦,任務被放在了阻塞隊列中,而負責放任務的線程是無法直接訪問到我們銀行具體實現轉賬操作的對象的,實現了隔離,提高了安全性。
4.阻塞隊列的特點
阻塞隊列區別於其他類型的隊列的最主要的特點就是 阻塞
這兩個字,所以下面重點介紹阻塞功能:阻塞功能使得生產者和消費者兩端的能力得以平衡,當有任何一端速度過快時,阻塞隊列便會把過快的速度給降下來。實現阻塞最重要的兩個方法是 take
方法和 put
方法。
4.1 take 方法
take 方法的功能是獲取並移除隊列的頭結點,通常在隊列裏有數據的時候是可以正常移除的。可是一旦執行 take 方法的時候,隊列裏無數據,則阻塞,直到隊列裏有數據。一旦隊列裏有數據了,就會立刻解除阻塞狀態,並且取到數據。過程如圖所示:
4.2 put 方法
put 方法插入元素時,如果隊列沒有滿,那就和普通的插入一樣是正常的插入,但是如果隊列已滿,那麼就無法繼續插入,則阻塞,直到隊列裏有了空閒空間。如果後續隊列有了空閒空間,比如消費者消費了一個元素,那麼此時隊列就會解除阻塞狀態,並把需要添加的數據添加到隊列中。過程如圖所示:
4.3 是否有界
阻塞隊列還有一個非常重要的屬性,那就是容量的大小,分爲有界和無界兩種。
無界隊列意味着裏面可以容納非常多的元素,例如 LinkedBlockingQueue 的上限是 Integer.MAX_VALUE,約爲 2 的 31 次方,是非常大的一個數,可以近似認爲是無限容量。
有界隊列可以設置固定大小,例如本章的示例設置 ArrayBlockingQueue 的大小爲 3 ,如果隊列滿了,也不會擴容,所以一旦滿了就無法再往裏放數據了,put 方法會阻塞當前線程。
5.參考
- 《Java 併發編程 78 講》- 徐隆曦