JUC學習系列一(阻塞隊列BlockingQueue)

阻塞隊列 BlockingQueue

java.util.concurrent 包裏的 BlockingQueue 接口表示一個線程安放入和提取實例的隊列。本小節我將給你演示如何使用這個 BlockingQueue。

BlockingQueue 用法

BlockingQueue 通常用於一個線程生產對象,而另外一個線程消費這些對象的場景。下圖是對這個原理的闡述:

blocking-queue
一個線程往裏邊放,另外一個線程從裏邊取的一個 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)  

四組不同的行爲方式解釋:
  1. 拋異常:如果試圖的操作無法立即執行,拋一個異常。
  2. 特定值:如果試圖的操作無法立即執行,返回一個特定的值(常常是 true / false)。
  3. 阻塞:如果試圖的操作無法立即執行,該方法調用將會發生阻塞,直到能夠執行。
  4. 超時:如果試圖的操作無法立即執行,該方法調用將會發生阻塞,直到能夠執行,但等待時間不會超過給定值。返回一個特定值以告知該操作是否成功(典型的是 true / false)。
無法向一個 BlockingQueue 中插入 null。如果你試圖插入 null,BlockingQueue 將會拋出一個 NullPointerException。
可以訪問到 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非常適合做交換工作,生產者的線程和消費者的線程同步以傳遞某些信息、事件或者任務。


雙向鏈表阻塞隊列簡單瞭解了一下,這裏就不寫了。

發佈了76 篇原創文章 · 獲贊 46 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章