阻塞隊列 BlockingQueue
java.util.concurrent 包裏的 BlockingQueue 接口表示一個線程安放入和提取實例的隊列。本小節我將給你演示如何使用這個 BlockingQueue。BlockingQueue 用法
BlockingQueue 通常用於一個線程生產對象,而另外一個線程消費這些對象的場景。下圖是對這個原理的闡述:一個線程往裏邊放,另外一個線程從裏邊取的一個 BlockingQueue。
一個線程將會持續生產新對象並將其插入到隊列之中,直到隊列達到它所能容納的臨界點。也就是說,它是有限的。如果該阻塞隊列到達了其臨界點,負責生產的線程將會在往裏邊插入新對象時發生阻塞。它會一直處於阻塞之中,直到負責消費的線程從隊列中拿走一個對象。
負責消費的線程將會一直從該阻塞隊列中拿出對象。如果消費線程嘗試去從一個空的隊列中提取對象的話,這個消費線程將會處於阻塞之中,直到一個生產線程把一個對象丟進隊列。
BlockingQueue 的方法
BlockingQueue 具有 4 組不同的方法用於插入、移除以及對隊列中的元素進行檢查。如果請求的操作不能得到立即執行的話,每個方法的表現也不同。這些方法如下:拋異常 | 特定值 | 阻塞 | 超時 | |
---|---|---|---|---|
插入 | add(o) | offer(o) | put(o) | offer(o, timeout, timeunit) |
移除 | remove(o) | poll(o) | take(o) | poll(timeout, timeunit) |
檢查 | element(o) | peek(o) |
四組不同的行爲方式解釋:
- 拋異常:如果試圖的操作無法立即執行,拋一個異常。
- 特定值:如果試圖的操作無法立即執行,返回一個特定的值(常常是 true / false)。
- 阻塞:如果試圖的操作無法立即執行,該方法調用將會發生阻塞,直到能夠執行。
- 超時:如果試圖的操作無法立即執行,該方法調用將會發生阻塞,直到能夠執行,但等待時間不會超過給定值。返回一個特定值以告知該操作是否成功(典型的是 true / false)。
可以訪問到 BlockingQueue 中的所有元素,而不僅僅是開始和結束的元素。比如說,你將一個對象放入隊列之中以等待處理,但你的應用想要將其取消掉。那麼你可以調用諸如 remove(o) 方法來將隊列之中的特定對象進行移除。但是這麼幹效率並不高(譯者注:基於隊列的數據結構,獲取除開始或結束位置的其他對象的效率不會太高),因此你儘量不要用這一類的方法,除非你確實不得不那麼做。
BlockingQueue 的實現
BlockingQueue 是個接口,你需要使用它的實現之一來使用 BlockingQueue。java.util.concurrent 具有以下 BlockingQueue 接口的實現(Java 6):- ArrayBlockingQueue
- DelayQueue
- LinkedBlockingQueue
- PriorityBlockingQueue
- SynchronousQueue
一、數組阻塞隊列ArrayBlockingQueue
同過一個簡單的生產消費模型,可以看出效果。當隊列裏的存放到達最大容量是,生產者會阻塞着,得消費者取走消息。容器裏面有新的空間後,繼續存放。效果複製以下代碼,執行main方法就行。
生產者:
import java.util.concurrent.BlockingQueue;
/**
* @Author Loujitao
* @Date 2018/6/27
* @Time 17:42
* @Description:
*/
public class Producer implements Runnable{
private BlockingQueue MQList=null;
public Producer(BlockingQueue MQList) {
this.MQList = MQList;
}
@Override
public void run() {
try {
for (int a=1;a<10;a++){
//這裏我讓線程休眠0.5s,因爲從數據庫查詢數據或者操作數據都是耗時的
Thread.sleep(500);
String str=Thread.currentThread().getName();
MQList.put(str+"的"+a);
System.out.println("生產者"+str+"存放了"+a);
}
System.out.println("生產者生產完成消息了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
消費者:
import java.util.concurrent.BlockingQueue;
public class Cunsumer implements Runnable{
private BlockingQueue MQList=null;
public Cunsumer(BlockingQueue MQList) {
this.MQList = MQList;
}
@Override
public void run() {
try {
while (true){
Thread.sleep(2000);
System.out.println("消費者"+Thread.currentThread().getName()+"取得消息:"+MQList.take());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
執行代碼:
package com.steve.concurrentCollection;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueTest {
public static void main(String[] args) {
function01();
}
/**
* @Author Loujitao
* @Date 2018/6/27
* @Time 17:44
* @Description: ArrayBlockingQueue的底層使用的是ReentrantLock重入鎖
* 而且是默認false,然後走sync = fair ? new FairSync() : new NonfairSync();的非公平鎖
*/
public static void function01(){
BlockingQueue bq=new ArrayBlockingQueue(5);
//producer是一個線程任務類,執行的方法是往隊列裏放10條數據
Producer producer=new Producer(bq);
//consumer是線程任務類,執行方法是往隊列裏拿數據 他兩的隊列是同一個隊列,
Cunsumer cunsumer=new Cunsumer(bq);
new Thread(producer).start();
new Thread(cunsumer).start();
}
}
結果:
總結:這個實例中生產者等待的時間是500ms,消費者2000ms;是消費慢生產快的情況。執行的過程中,我發現生產者很快就存放玩了五條數據,而消費者還在消費第一條數據,但是隊列已經滿了,所以此時生產者阻塞在這裏,等待隊列有空餘位置。然後的情況就是,消費者消費掉一條數據,生產者立馬就放入數據。直到九條數據存放完畢,生產者的線程終止,只剩下消費者慢慢消費。生產和消費的數據都是安全的。
只要將兩個數據調換,就能模擬出生產慢、消費快的情景。
多生產者、單消費者情景
public static void function01(){
BlockingQueue bq=new ArrayBlockingQueue(5);
//producer是一個線程任務類,執行的方法是往隊列裏放10條數據
Producer producer=new Producer(bq);
Producer producer2=new Producer(bq);
//consumer是線程任務類,執行方法是往隊列裏拿數據 他兩的隊列是同一個隊列,
Cunsumer cunsumer=new Cunsumer(bq);
new Thread(producer,"pro1").start();
new Thread(producer2,"pro2").start();
new Thread(cunsumer).start();
}
模擬了一下這種情況,數據是安全的。多生產多消費者的情況,只要在新new出相應的對象,放到線程中執行即可,感興趣的可以嘗試一下。實際生產中可能應用的場景:(本人還沒碰到,一些中間件底層可能用到,但沒時間看源碼,今後再做補充吧)
二、鏈式阻塞隊列(LinkedBlockingQueue)
public static void function02(){
BlockingQueue bq=new LinkedBlockingQueue();
//producer是一個線程任務類,執行的方法是往隊列裏放10條數據
Producer producer=new Producer(bq);
Producer producer2=new Producer(bq);
//consumer是線程任務類,執行方法是往隊列裏拿數據 他兩的隊列是同一個隊列,
Cunsumer cunsumer=new Cunsumer(bq);
new Thread(producer,"pro1").start();
// new Thread(producer2,"pro2").start();
new Thread(cunsumer).start();
}
還是使用以上的生產-消費模型,只是我這次創建的隊列是無界的鏈式隊列。運行的結果如下:總結:因爲使用的隊列是無界的,它能容納的個數是Integer.MAX_VALUE,整型的最大值。所以不存在生產者沒地方存放消息,而需要阻塞的情況。生產者依次存放消息,存滿了線程結束;消費者慢慢消費,直到所有的情況消費完畢。這種情況是生產快、消費慢的情況。
如果你將500ms和2000ms調換一下,即消費快、生產慢的時候,就會發現消費者一直隊列中一有消息進來,就被消費者消費掉,沒有消息就一直阻塞等待着。
像多生產者和多消費者這些情況,同上一個隊列,可以實例化多個實例,模擬下不同場景,但是數據一定是安全的。
兩個生產者一個消費者,在消費快的情況下:
實際生產中可能應用的場景:(待續)
三、優先級隊列(PriorityBlockingQueue)
帶優先級的無界阻塞隊列,每次出隊都返回優先級最高的元素,是二叉樹最小堆的實現,研究過數組方式存放最小堆節點的都知道,直接遍歷隊列元素是無序的。該實現類需要自己實現一個繼承了 Comparator 接口的類, 在插入資源時會按照自定義的排序規則來對資源數組進行排序。
示例:
比較元素類
public class MyTask implements Comparable<MyTask> {
private int money;
private String name;
@Override
public int compareTo(MyTask o) {
return this.money>o.money?-1:(this.money==o.money?0:1);
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "money=" + money +
",name=" + name +" ";
}
}
測試類:
public class PriorityBlockingQueueTest {
/**
* @Author Loujitao
* @Date 2018/7/2
* @Time 9:58
* @Description: https://blog.csdn.net/qq_38872310
*/
public static void main(String[] args)throws Exception {
BlockingQueue<MyTask> bq=new PriorityBlockingQueue<MyTask>();
MyTask t0=new MyTask();
t0.setMoney(0);
t0.setName("VIP LV0");
MyTask t1=new MyTask();
t1.setMoney(1);
t1.setName("VIP LV1");
MyTask t2=new MyTask();
t2.setMoney(2);
t2.setName("VIP LV2");
MyTask t3=new MyTask();
t3.setMoney(3);
t3.setName("VIP LV3");
//這裏我特意打亂了順序
bq.add(t2);
bq.add(t1);
bq.add(t3);
bq.add(t0);
//優先級最高的先執行了,這裏t3的VIP等級高先走了。(社會你懂的)
//優先級隊列,只有調用一次take()方法,纔會進行排序
bq.take();//System.out.println(bq.take());
System.out.println(bq);
}
}
執行結果:
總結:bq.take()的返回值是你隊列裏存放的對象,這裏的是mytask。可以用System.out.println(bq.take())查看。
PriorityBlockingQueue始終保證出隊的元素是優先級最高的元素,並且可以定製優先級的規則,內部通過使用一個二叉樹最小堆算法來維護內部數組,這個數組是可擴容的,噹噹前元素個數>=最大容量時候會通過算法擴容。值得注意的是爲了避免在擴容操作時候其他線程不能進行出隊操作,實現上使用了先釋放鎖,然後通過cas保證同時只有一個線程可以擴容成功。
如果想深入探究,可以瞭解下二叉樹的數據結構,自己看下這個隊列的源碼。
使用場景:
四、延遲隊列( DelayQueue)
DelayQueue是一個無界阻塞隊列,只有在延遲期滿時才能從中提取元素。該隊列的頭部是延遲期滿後保存時間最長的Delayed 元素。爲了具有調用行爲,存放到DelayDeque的元素必須繼承Delayed接口。Delayed接口使對象成爲延遲對象,它使存放在DelayQueue類中的對象具有了激活日期。該接口強制執行下列兩個方法。
- CompareTo(Delayed o):Delayed接口繼承了Comparable接口,因此有了這個方法。
- getDelay(TimeUnit unit):這個方法返回到激活日期的剩餘時間,時間單位由單位參數指定
示例:
用戶對象
public class LOLGamer implements Delayed {
private String name;
//上機座位號
private String id;
//截止時間
private long endTime;
//定義時間工具類
private TimeUnit timeUnit = TimeUnit.SECONDS;
public LOLGamer(String name, String id, long endTime) {
this.name = name;
this.id = id;
this.endTime = endTime;
}
@Override
public int compareTo(Delayed o) {
LOLGamer lg=(LOLGamer) o;
return this.getDelay(this.timeUnit)-o.getDelay(this.timeUnit)>0?1:0;
}
@Override
public long getDelay(TimeUnit unit) {
return endTime-System.currentTimeMillis();
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public long getEndTime() { return endTime; }
public void setEndTime(long endTime) { this.endTime = endTime; }
}
執行代碼
public class GameBar implements Runnable{
private DelayQueue<LOLGamer> queue = new DelayQueue<LOLGamer>();
public boolean yinye =true;
public void shangji(String name,String id,int money){
//1000 * money + System.currentTimeMillis() 表示從當前開始上1000ms,截止下機時間就是這個和
LOLGamer man = new LOLGamer(name, id, 1000 * money + System.currentTimeMillis());
System.out.println("玩家"+man.getName()+" 機位號"+man.getId()+"交錢"+money+"塊,開始上機...");
//上機同時將消費記錄存到延遲隊列裏
this.queue.add(man);
}
public void xiaji(LOLGamer man){
System.out.println("玩家"+man.getName()+" 機位號"+man.getId()+"時間到下機...");
}
@Override
public void run() {
while(yinye){
try {
//延遲時間到了,才能獲取數據 只有有數據才能取到
LOLGamer man = queue.take();
//時間到了的牆紙下機了
xiaji(man);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String args[]){
try{
System.out.println("網魚網吧歡迎你");
GameBar wangyu = new GameBar();
Thread shangwang = new Thread(wangyu);
shangwang.start();
wangyu.shangji("張三", "A001", 1);
wangyu.shangji("李四", "A002", 10);
wangyu.shangji("王五", "A003", 5);
}
catch(Exception e){
e.printStackTrace();
}
}
}
輸出:
總結:DelayQueue類的主要作用:是一個無界的BlockingQueue,用於放置實現了Delayed接口的對象,其中的對象只能在其到期時才能從隊列中取走。這種隊列是有序的,即隊頭對象的延遲到期時間最長。注意:不能將null元素放置到這種隊列中。
Delayed,一種混合風格的接口,用來標記那些應該在給定延遲時間之後執行的對象。此接口的實現必須定義一個 compareTo 方法,該方法提供與此接口的 getDelay 方法一致的排序。
應用場景: DelayQueue阻塞隊列在我們系統開發中也常常會用到,例如:緩存系統的設計,緩存中的對象,超過了空閒時間,需要從緩存中移出;任務調度系統,能夠準確的把握任務的執行時間。
多考試上機考試的場景:來自於http://ideasforjava.iteye.com/blog/657384,模擬一個考試的日子,考試時間爲120分鐘,30分鐘後纔可交卷,當時間到了,或學生都交完捲了考試結束。
具有時間的緩存場景:來自於http://www.cnblogs.com/jobs/archive/2007/04/27/730255.html,向緩存添加內容時,給每一個key設定過期時間,系統自動將超過過期時間的key清除。
五、同步隊列(SynchronousQueue)
特點:1、SynchronousQueue沒有容量。與其他BlockingQueue不同,SynchronousQueue是一個不存儲元素的BlockingQueue.每一個put操作必須要等待一個take操作,否則不能繼續添加元素,反之亦然。
2、因爲沒有容量,所以對應 peek, contains, clear, isEmpty … 等方法其實是無效的。例如clear是不執行任何操作的,contains始終返回false,peek始終返回null。
3、SynchronousQueue分爲公平和非公平,默認情況下采用非公平性訪問策略,當然也可以通過構造函數來設置爲公平性訪問策略(爲true即可)。
4、若使用 TransferQueue, 則隊列中永遠會存在一個 dummy node(這點後面詳細闡述)。
示例:
/**
* @Author Loujitao
* @Date 2018/7/2
* @Time 10:45
* @Description:
* 不像ArrayBlockingQueue、LinkedBlockingDeque之類的阻塞隊列依賴AQS實現併發操作,
* SynchronousQueue直接使用CAS實現線程的安全訪問。它是個沒有容量的隊列,
*/
public static void main(String[] args) throws Exception {
final BlockingQueue<String> bq=new SynchronousQueue<>();
//第一種情況 這種直接添加,會出異常Queue full
//bq.add("steve");
//第二種情況 如果有線程先取,會阻塞着,這樣就能放進去
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println( bq.take());
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(1000);
//可以理解爲交接給了等待的線程,不是真正的存放進去了
bq.put("stevetao");
}
結果截圖:
總結:SynchronousQueue由於其獨有的線程一一配對通信機制,在大部分平常開發中,可能都不太會用到,但線程池技術中會有所使用,由於內部沒有使用AQS,而是直接使用CAS,所以代碼理解起來會比較困難。這裏只是做了簡介和使用,具體底層實現原理,我覺得有必要在搞清CAS和此隊列源碼後,單獨寫一篇博客以作說明。
應用場景:SynchronousQueue非常適合做交換工作,生產者的線程和消費者的線程同步以傳遞某些信息、事件或者任務。
雙向鏈表阻塞隊列簡單瞭解了一下,這裏就不寫了。