阻塞佇列和生產者-消費者模式

貓毛·波拿巴發表於2018-09-03

何為阻塞佇列,其與普通佇列有何差別?

  總的來說,就是能夠在適當的時候阻塞"存"和"取"兩個操作,以達到控制任務流程的效果。阻塞佇列提供了可阻塞的put和take方法。如果佇列已經滿了,那麼put方法將阻塞直到有空間可用;如果佇列為空,那麼take方法將會阻塞直到有元素可用。

阻塞佇列介面及實現來自於Java併發包(java.util.concurrent),常見的實現有LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue

 

生產者-消費者模式

  生產者-消費者模式是非常常見的設計模式。該模式將"找出需要完成的工作"與"執行工作"這兩個過程分離開來,並把工作項放入一個"待完成"列表中以便在隨後處理,而不是找出後立即處理。生產者-消費者模式能簡化開發過程,因為它消除了生產者類與消費者類之間的程式碼依賴性,此外,該模式還將生產資料的過程與使用資料的過程解耦開來以簡化工作負載的管理,因為這兩個過程在處理資料的速率上有所不同。

 

 

阻塞佇列對於生產者-消費者模式有何裨益?

  生產者-消費者模式都是基於佇列的。就說說普通的有界佇列存在的問題吧,佇列存在"滿"和"空"的問題,如果佇列已滿,那生產者繼續往佇列裡存資料就會出問題,存不進去要如何處理,生產者程式碼中就要有相應的處理程式碼。同樣的,如果佇列為空,消費者取不到資料又要如何反應。而阻塞佇列,就可以在"存不進"和"取不出"的時候,直接阻塞操作,生產者和消費者程式碼直接阻塞在存取操作上。當然這種阻塞並不是永久的,就拿生產者來說吧,如果因為"存不進"而阻塞的話,只要消費者取出資料,便會"通知"生產者就能繼續生產並儲存資料。這樣就能極大地簡化生產者-消費者的編碼。

  值得一提的是,阻塞佇列還能提供更靈活的選項:offer(對應put)和 poll(對應take)

boolean offer(E e);

boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

  如果資料項不能被新增到佇列中,將返回一個失敗狀態。而不必一直阻塞下去。這樣你就可以選擇讓生產者做點其他的事。但是一般情況下,如果佇列充滿,很有可能是因為

V生>V消,以至於資料項囤積,如果任其阻塞,則生產者可能被長時間擱置,浪費資源,利用率降低。這時候就要使用一些靈活的策略進行調控,例如減去負載,將多餘的工作項序列化並寫入磁碟,減少生產者執行緒的數量,或者通過某種方式來抑制生產者執行緒。

 

使用示例

示例說明:本示例模擬一個生產-消費環境,工廠生產可樂,肥宅消費。這裡對於生產者的調控比較粗暴,直接新建或中斷一個生產者任務。

public class BlockingQueueDemo {

    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<CocaCola> queue = new ArrayBlockingQueue<>(100); //容量100的佇列
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            exec.execute(new Producer(queue, exec));
        }
        TimeUnit.SECONDS.sleep(3); //先生產一點庫存
        for (int i = 0; i < 5; i++) {
            exec.execute(new FatIndoorsman(queue, exec));
        }
    }
}

class CocaCola { //可口可樂

}

class Producer implements Runnable {
    private static int counter = 0;
    private final int id = counter++;
    private static List<Producer> producers = new ArrayList<>(); //類管理其例項列表
    private Executor exec;
    private BlockingQueue queue;

    public Producer(BlockingQueue queue, Executor exec) {
        this.queue = queue;
        this.exec = exec;
        producers.add(this);
    }

    public synchronized static void adjust(int flag, BlockingQueue queue, Executor exec) { // 1 新增  -1減少
        if (flag == 1) {
            Producer producer = new Producer(queue, exec); //新增的生產者共享同一個佇列
            exec.execute(producer);
        } else if (flag == -1) {
            Producer producer = producers.remove(0);
            producer.cancel();
        }
    }

    private void cancel() { //利用中斷取消生產任務
        Thread.currentThread().interrupt();
    }

    @Override
    public void run() {
        while (!Thread.interrupted()) {
            try {
                TimeUnit.SECONDS.sleep(1); //模擬生產需耗時1秒
                boolean success = queue.offer(new CocaCola()); //通過offer嘗試新增
                if (!success) { //如果佇列已滿,則移除1個生產者
                    System.out.println("remove a producer");
                    adjust(-1, queue, exec);
                }
                System.out.println(this + " produced a coca-cola!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(this + " is stoped!");
    }

    @Override
    public String toString() {
        return "Producer[" + id + "]";
    }
}

class FatIndoorsman implements Runnable {
    private static int counter = 0;
    private final int id = counter++;
    private BlockingQueue queue;
    private Executor exec;

    public FatIndoorsman(BlockingQueue queue, Executor exec) {
        this.queue = queue;
        this.exec = exec;
    }

    @Override
    public void run() {
        while (!Thread.interrupted()) {
            CocaCola cocaCola = (CocaCola) queue.poll();
            if (cocaCola != null) {
                try {
                    TimeUnit.SECONDS.sleep(10); //模擬肥宅每隔10秒要喝一瓶
                    System.out.println(this + " drink a coca-cola");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                Producer.adjust(1, queue, exec); //新增生產者
            }
        }
    }

    @Override
    public String toString() {
        return "FatIndoorsman[" + id + "]";
    }
}

 

 

 

阻塞佇列是如何實現的,即如何進行阻塞?

顯示鎖Lock+條件佇列Condition。這裡維護兩個條件佇列,對應take和put操作。兩個Condition繫結同一個Lock(即由同一個Lock.newCondition生成)。拿ArrayBlockingQueue來說,其他實現類實現阻塞的方式應該類似。如果某一方take和put失敗,則呼叫對應Condition的await方法,呼叫的執行緒將被阻塞,進入等待條件的佇列,並釋放鎖。如果某一方成功(即成功呼叫dequeue或enqueue方法),則會呼叫對應的Condition的signal方法,等待條件佇列中的某個執行緒將被選中並啟用。

 

為何要使用Lock+Condition,而不是隱式的synchronized和wait/notify條件佇列?

  首先我們有兩個需要,第一,我們需要兩個條件佇列來維護take和put操作,而一個物件Object只繫結一個條件佇列,如果觸發notify(),我們不知道到底是哪個條件達到了。不過我們可以建立兩個內部的全域性變數Object作為鎖,將這兩個物件的內建鎖作為take和put操作的同步鎖,這樣就可以有兩個條件佇列了。但是,我們說了,還有一個需要,那就是take和put操作必須共有同一個鎖。

2018-09-11 20:28:07 補充:內建的條件佇列也可以實現多個條件控制,條件佇列裡存的是等待條件的執行緒,每個執行緒等待的條件可以不一樣。notifyAll能夠使得所有執行緒甦醒,並檢查條件謂詞,檢視是否達到條件,如果未達到條件,則繼續wait。達到則向下執行。這裡引入顯式的條件佇列Condition的作用是,分離等待條件的執行緒,將等待同一條件的執行緒分配到同一個佇列,這時候,就可以只喚醒等待特定條件的執行緒了。

注意:notifyAll並沒有關聯什麼條件謂詞,而是提示所有執行緒,某個狀態發生改變了,你們自己看看是不是自己等待的條件達到了。

 

以下是ArrayBlockingQueue.java的部分原始碼截圖

 

 

 

相關文章