生產者消費者模型

classic123發表於2023-02-23

生產者消費者模型

什麼是生產者消費者模型

我們可以把這個模型想象成工廠裡的兩條流水線,我們管他們叫生產者流水線和消費者流水線,生產者流水線生產出來的產品給消費者流水線使用,其中生產者流水線先把生產出來的產品放在倉庫,然後消費者流水線再去倉庫拿。這個倉庫就叫做阻塞佇列。
那麼,這個倉庫的實現有什麼要求呢?

  • 第一,倉庫滿的時候,不能繼續往裡放了,生產者流水線要停止生產。
  • 第二,倉庫空的時候,拿不出來了,消費者流水線要停止取商品。
  • 第三,生產者流水線和消費者流水線都可以使用倉庫。也就是說倉庫是共享變數,要注意執行緒安全。

下面我們先來設計一下這個倉庫(阻塞佇列):
關於阻塞佇列的設計,有幾點需要我們思考:

  • ①為什麼要給阻塞佇列容量的限制?如果生產者生成的效率比消費者消費的效率快,那麼生產者產生的“產品”會不斷的在佇列中累積起來,最終耗盡記憶體。如果使用有界佇列,那麼當佇列滿時,生產者會阻塞並且不能繼續工作,而消費者可以趕上工作進度。
  • ②如何保證阻塞佇列的執行緒安全?由於生產者和消費者都是對阻塞佇列進行修改操作,所以我們既需要保證操作的可見性又需要保證原子性,實現的方法有很多種:一種是將阻塞佇列設計成執行緒安全的類,另一種是將生產者和消費者對阻塞佇列的操作設計成原子操作。
  • ③阻塞佇列是如何實現阻塞的?wait/notify 或者 await/signal都可以實現阻塞與喚醒操作。

生產者消費者模型的程式碼實現

將阻塞佇列設計成執行緒安全的類

import java.util.LinkedList;
import java.util.Queue;

public class BlockQueueplus {
    Queue<Integer> queue = new LinkedList();
    int capacity ; //阻塞佇列的容量

    public BlockQueueplus(int capacity) {
        this.capacity = capacity;

    }

    /**
     * 將資料放入阻塞佇列中
     * @param i 放入的元素
     * @throws InterruptedException
     */
    public synchronized void put(Integer i) throws InterruptedException {
        while(capacity <= queue.size()){
            wait();
        }
        queue.offer(i);
        System.out.println(Thread.currentThread().getName() + "生產了value, value的當前值是" + i );
        notify();
    }

    /**
     * 從阻塞佇列中取出資料
     * @return
     * @throws InterruptedException
     */
    public synchronized int take() throws InterruptedException {
        if(size() == 0){
            wait();
        }
        Integer result = queue.poll();
        System.out.println(Thread.currentThread().getName() + "消費了value, value的當前值是" + result );
        notify();
        return result;
    }

    public int size(){
        return queue.size();
    }

    public Boolean isEmpty(){
        return queue.isEmpty();
    }

    public Boolean isFull(){
        return this.size()==this.capacity;
    }

}

synchronized關鍵字用在方法上的作用

有的同學可能不懂synchronized關鍵字用在方法上有什麼作用,我來講解一下:
在我的BlockQueueplus類中,put()和take()都新增了synchronized關鍵字,當進入synchronized修飾的方法時,鎖住的是當前例項類,所以當呼叫put()方式時,put()方法拿到了當前例項的鎖,take()想執行也需要拿到鎖,就要等待put()方法執行完釋放鎖。所以就實現了put操作和take操作的互斥。

wait和notify的使用

首先呢,Java 中每個物件都有一把稱之為 monitor 監視器的鎖,呼叫synchronized方法時,會獲取monitor鎖。當呼叫wait方法時,會釋放monitor鎖。在我的BlockQueueplus類中,當佇列滿時,put方法會呼叫wait方法,進入阻塞,此時take方法就可以獲取鎖,執行取操作。take操作在執行完取操作之後,會呼叫notify()方法,通知一個正在wait阻塞中的執行緒讓它繼續執行。

生產者執行緒與消費者執行緒

用Work把阻塞佇列封裝一下,只提供插入和取兩種方法。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class Work{
    private static BlockQueueplus blockQueueplus = new BlockQueueplus(100);
    public void set(int i)
    {
            try {
                blockQueueplus.put(i);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
    }

    public void get() 
    {
        try {
            Integer i = blockQueueplus.take();
            
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

測試類

開啟兩個執行緒,生產者不停的往阻塞佇列中插入資料,消費者不停的從阻塞佇列中取資料

class WorkTest {
    public static void main(String[] args) {
        Work work = new Work();
        Runnable producerRunnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < Integer.MAX_VALUE; i++)
                    work.set(i);
            }
        };
        Runnable customerRunnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < Integer.MAX_VALUE; i++)
                    work.get();
            }
        };
        Thread ProducerThread = new Thread(producerRunnable);
        ProducerThread.setName("Producer");
        Thread ConsumerThread = new Thread(customerRunnable);
        ConsumerThread.setName("Consumer");
        ProducerThread.start();
        ConsumerThread.start();
    }
}

執行結果:

從執行結果中可以發現,生產者執行緒與消費者執行緒交接的時候,他們生產的數和消費的數的差正好為99,也證明了阻塞佇列設計的成功。

還可以改進的地方

生產者執行緒可能是多個,消費者執行緒也可以是多個,如果繼續使用暴力的wait和notify,就有可能會出現生產者A喚醒生產者B的錯誤,我們可以嘗試使用await和signal來優雅的喚醒需要喚醒的執行緒。

相關文章