生產者消費者模型
什麼是生產者消費者模型
我們可以把這個模型想象成工廠裡的兩條流水線,我們管他們叫生產者流水線和消費者流水線,生產者流水線生產出來的產品給消費者流水線使用,其中生產者流水線先把生產出來的產品放在倉庫,然後消費者流水線再去倉庫拿。這個倉庫就叫做阻塞佇列。
那麼,這個倉庫的實現有什麼要求呢?
- 第一,倉庫滿的時候,不能繼續往裡放了,生產者流水線要停止生產。
- 第二,倉庫空的時候,拿不出來了,消費者流水線要停止取商品。
- 第三,生產者流水線和消費者流水線都可以使用倉庫。也就是說倉庫是共享變數,要注意執行緒安全。
下面我們先來設計一下這個倉庫(阻塞佇列):
關於阻塞佇列的設計,有幾點需要我們思考:
- ①為什麼要給阻塞佇列容量的限制?如果生產者生成的效率比消費者消費的效率快,那麼生產者產生的“產品”會不斷的在佇列中累積起來,最終耗盡記憶體。如果使用有界佇列,那麼當佇列滿時,生產者會阻塞並且不能繼續工作,而消費者可以趕上工作進度。
- ②如何保證阻塞佇列的執行緒安全?由於生產者和消費者都是對阻塞佇列進行修改操作,所以我們既需要保證操作的可見性又需要保證原子性,實現的方法有很多種:一種是將阻塞佇列設計成執行緒安全的類,另一種是將生產者和消費者對阻塞佇列的操作設計成原子操作。
- ③阻塞佇列是如何實現阻塞的?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來優雅的喚醒需要喚醒的執行緒。