[Java併發程式設計實戰] 阻塞佇列 BlockingQueue(含程式碼,生產者-消費者模型)

weixin_34320159發表於2018-06-02

見賢思齊焉,見不賢而內自省也。—《論語》

Java5.0 增加了兩種新的容器型別,它們是指:Queue 和 BlockingQueue。Queue 用來臨時儲存一組等待處理的元素。BlockingQueue 擴張了 Queue 介面,增加了可阻塞的插入和獲取等操作。

BlockingQueue 通常運用於一個執行緒生產物件放入佇列,另一個執行緒從佇列獲取物件並消費,這是典型的生產者消費者模型。


3528880-b9adfca9d36c5e5e
這裡寫圖片描述

生產者執行緒持續生產新物件並插入佇列,如果佇列已滿,那麼插入物件的操作會一直阻塞,直到佇列中出現可用的空間。
消費者執行緒持續從佇列獲取物件,如果佇列為空,那麼獲取操作會一直阻塞,直到佇列中出現可用的新物件。BlockingQueue 簡化了生產者-消費者設計的實現過程,它支援任意數量的生產者和消費者。

BlockingQueue 的核心方法:

3528880-d43790f280422c35
這裡寫圖片描述

offer(E): 向佇列插入元素,並返回插入成功與否。本方法不阻塞當前執行執行緒。

put(E) : 向佇列插入元素,如果佇列已滿,則會阻塞當前執行緒直至元素加入佇列。

take() : 獲取佇列的首位元素,如果佇列為空,則阻塞當前執行緒直至佇列有元素並取走。

poll():獲取佇列首個元素,指定時間內一旦資料可取,則立即返回;否則返回失敗。

remove(E):刪除佇列中的元素,返回成功與否。

BlockingQueue 的實現

BlockingQueue是一個介面,所以你必須使用它的實現來使用它。它的實現包括以下幾個:

  • ArrayBlockingQueue:基於陣列實現的有界佇列(FIFO),使用一把全域性鎖並行對 queue 讀寫操作,同時使用兩個 Condition 阻塞佇列為空時的取操作和佇列為滿時的寫操作。

  • LinkedBlockingQueue:基於已連結節點的,範圍上限為 Integer.MAX_VALUE 的 blocking queue(FIFO)。主要操作 put 和 take 都是阻塞的。

  • DelayQueue:當指定的延遲時間到了,才能夠從佇列中獲取元素。它沒有大小限制,因此插入元素時不會阻塞,而只有獲取元素時才會被阻塞。它的用法可以參考下面兩篇部落格:
    http://www.cnblogs.com/jobs/archive/2007/04/27/730255.html
    http://www.cnblogs.com/sunzhenchao/p/3515085.html

  • PriorityBlockingQueue: 基於優先順序的阻塞佇列,但需要注意的是PriorityBlockingQueue並不會阻塞資料生產者,而只會在沒有可消費的資料時,阻塞資料的消費者。

  • SynchronoutQueue:它的內部同時只能夠容納單個元素。如果該佇列已有一個元素的話,試圖向佇列插入一個新元素執行緒會阻塞,知道另一個執行緒將該元素從佇列中拿走。同樣,如果該佇列為空,試圖向佇列中抽取一個元素的執行緒將會阻塞,知道另一個執行緒向佇列中插入一個新的元素。SynchronousQueue適合一對一的匹配場景,沒有容量,無法快取。它的用法強烈推薦部落格:
    http://www.cnblogs.com/leesf456/p/5560362.html

BlockingQueue的使用

這是一個使用 BlockingQueue 的例子,本例使用 ArrayBlockingQueue 實現。首先,BlockingQueueTest 建立一個生產者執行緒 Procucer, 把字元存放進共享佇列。然後建立三個消費者執行緒 Consumer,把字串從佇列中取出。Consumer 取到最後一個字串時,中斷所有消費者執行緒,結束程式。

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class BlockingQueueTest {
    //佇列容量
    private static final int SIZE = 5;
    private static final int CONSUMER_SIZE = 3;
    //消費者執行緒退出標誌
    private static String endString = "num:" + (SIZE*2-1);
    //存放消費者執行緒
    private static List list = new ArrayList<Thread>();
    
    public static void main(String[] args) throws Exception{
        //建立固定長度的阻塞佇列
        BlockingQueue q = new ArrayBlockingQueue<String>(SIZE);
        
        //建立生產者
        Producer producer = new Producer(q);
        //啟動生產者執行緒,生產物件
        producer.start();
        
        //啟動消費者執行緒,獲取佇列物件
        for(int i = 0; i < CONSUMER_SIZE; i++) {
            list.add(new Consumer(q));
            ((Thread) list.get(i)).start();
        }
    }
    
    //中斷執行緒
    public static void shutDownThread() {
        for(int i = 0; i < CONSUMER_SIZE; i++) {
            ((Thread) list.get(i)).interrupt();
        }
    }

    static class Producer extends Thread{

        private BlockingQueue queue = null;
        
        public Producer(BlockingQueue q) {
            this.queue = q;
        }
        
        @Override
        public void run() {
            // TODO Auto-generated method stub
            try {
                //生產10個物件,放進佇列
                for(int i = 0; i < SIZE*2; i++) {
                    String str = "num:" + i;
                    System.out.println(Thread.currentThread().getName() +":"+"IN: " + str);
                    queue.put(str);
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            
        }
    }
    
    //消費者執行緒
    static class Consumer extends Thread{

        private BlockingQueue queue = null;
        
        public Consumer(BlockingQueue q) {
            this.queue = q;
        }
        
        @Override
        public void run() {
            // TODO Auto-generated method stub
            try {
                //獲取佇列的元素,佇列為空時會阻塞
                while(true) {
                    String str = (String) queue.take();
                    System.out.println(Thread.currentThread().getName() + ":" + "OUT " + str);
                    //已經取出最後一個元素,消費者執行緒應該退出,否則程式一直在執行
                    if(str.equals(endString)) {
                        shutDownThread();//中斷執行緒
                    }
                }
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}


執行結果:

3528880-a313f9e4d0015706
這裡寫圖片描述

總結

java.util.concurrent 中實現的阻塞佇列包含了足夠的同步機制,從而能夠安全的將物件從生產者執行緒釋出到消費者執行緒。對於可變物件,生產者-消費者模型,把物件的所有權安全的從生產者交付給消費者。轉移後,消費者執行緒獲得這個物件的所有權(獨佔訪問權,可以任意修改它),並且生產者執行緒不會再訪問它。同時,阻塞佇列負責所有的控制流,使得消費者生產者的程式碼更加簡單和清晰。

本文完畢,如對你有幫助,請關注我,謝謝~

參考

  1. https://blog.csdn.net/defonds/article/details/44021605/
  2. http://www.cnblogs.com/leesf456/p/5428630.html
  3. 《併發程式設計實戰》

相關文章