對於多線程程序來說,不管任何編程語言,生產者和消費者模型都是最經典的。就像學習每一門編程語言一樣,Hello World!都是最經典的例子。
實際上,準確說應該是“生產者-消費者-倉儲”模型,離開了倉儲,生產者消費者模型就顯得沒有說服力了。
對於此模型,應該明確一下幾點:
1、生產者僅僅在倉儲未滿時候生產,倉滿則停止生產。
2、消費者僅僅在倉儲有產品時候才能消費,倉空則等待。
3、當消費者發現倉儲沒產品可消費時候會通知生產者生產。
4、生產者在生產出可消費產品時候,應該通知等待的消費者去消費。
Demo實列:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author lishujiang
* @date 2020/4/11 15:11
*/
public class ProducerConsumer {
//代表生產的商品數量
private static AtomicInteger count = new AtomicInteger(0);
/**
* 消費者
*/
class Consumer implements Runnable {
private String name;
private Storage s = null;
public Consumer(String name, Storage s) {
this.name = name;
this.s = s;
}
public void run() {
try {
while (true) {
Thread.sleep(5001);
System.out.println(name + "準備消費產品.");
Product product = s.pop();
System.out.println(name + "已消費(" + product.toString() + ").");
System.out.println("===============");
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 生產者
*/
class Producer implements Runnable {
private String name;
private Storage s = null;
public Producer(String name, Storage s) {
this.name = name;
this.s = s;
}
public void run() {
try {
while (true) {
Product product = new Product(count.incrementAndGet()); // 產生產品
System.out.println(name + "準備生產(" + product.toString() + ").");
s.push(product);
System.out.println(name + "已生產(" + product.toString() + ").");
System.out.println("===============");
Thread.sleep(500);
}
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
/**
* 倉庫容量默認設置爲10,阻塞隊列生產消費線程安全
*/
public class Storage {
BlockingQueue<Product> queues = new LinkedBlockingQueue<Product>(10);
/**
* 生產
*
* @param p 產品
* @throws InterruptedException
*/
public void push(Product p) throws InterruptedException {
queues.put(p);
}
/**
* 消費
*
* @return 產品
* @throws InterruptedException
*/
public Product pop() throws InterruptedException {
return queues.take();
}
}
/**
* 產品
*/
public class Product {
private int id;
public Product(int id) {
this.id = id;
}
public String toString() {// 重寫toString方法
return "產品:" + this.id;
}
}
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();
Storage s = pc.new Storage();
ExecutorService service = Executors.newCachedThreadPool();
Producer p = pc.new Producer("張三", s);
Producer p2 = pc.new Producer("李四", s);
Consumer c = pc.new Consumer("王五", s);
Consumer c2 = pc.new Consumer("老劉", s);
Consumer c3 = pc.new Consumer("老林", s);
service.submit(p);
service.submit(p2);
service.submit(c);
// service.submit(c2);
// service.submit(c3);
}
}
運行結果
張三準備生產(產品:1).
張三已生產(產品:1).
===============
李四準備生產(產品:2).
李四已生產(產品:2).
===============
張三準備生產(產品:3).
張三已生產(產品:3).
===============
李四準備生產(產品:4).
李四已生產(產品:4).
===============
張三準備生產(產品:5).
張三已生產(產品:5).
===============
李四準備生產(產品:6).
李四已生產(產品:6).
===============
李四準備生產(產品:7).
李四已生產(產品:7).
===============
張三準備生產(產品:8).
張三已生產(產品:8).
===============
李四準備生產(產品:9).
李四已生產(產品:9).
===============
張三準備生產(產品:10).
張三已生產(產品:10).
===============
李四準備生產(產品:11).
張三準備生產(產品:12).
王五準備消費產品.
李四已生產(產品:11).
===============
王五已消費(產品:1).
===============
李四準備生產(產品:13).
阻塞隊列BlockingQueue用法
多線程環境中,通過隊列可以很容易實現數據共享,比如經典的“生產者”和“消費者”模型中,通過隊列可以很便利地實現兩者之間的數據共享。
假設我們有若干生產者線程,另外又有若干個消費者線程。如果生產者線程需要把準備好的數據共享給消費者線程,利用隊列的方式來傳遞數據,就可以很方便地解決他們之間的數據共享問題。但如果生產者和消費者在某個時間段內,萬一發生數據處理速度不匹配的情況呢?理想情況下,如果生產者產出數據的速度大於消費者消費的速度,並且當生產出來的數據累積到一定程度的時候,那麼生產者必須暫停等待一下(阻塞生產者線程),以便等待消費者線程把累積的數據處理完畢,反之亦然。
然而,在concurrent包發佈以前,在多線程環境下,我們每個程序員都必須去自己控制這些細節,尤其還要兼顧效率和線程安全,而這會給我們的程序帶來不小的複雜度。好在此時,強大的concurrent包橫空出世了,而他也給我們帶來了強大的BlockingQueue。(在多線程領域:所謂阻塞,在某些情況下會掛起線程(即阻塞),一旦條件滿足,被掛起的線程又會自動被喚醒)
BlockingQueue的兩個常見阻塞場景:
1、當隊列中沒有數據的情況下,消費者端的所有線程都會被自動阻塞(掛起),直到有數據放入隊列。
2、當隊列中填滿數據的情況下,生產者端的所有線程都會被自動阻塞(掛起),直到隊列中有空閒的位置,線程被自動喚醒。
這也是我們在多線程環境下,爲什麼需要BlockingQueue的原因。作爲BlockingQueue的使用者,我們再也不需要關心什麼時候需要阻塞線程,什麼時候需要喚醒線程,因爲這一切BlockingQueue都給你一手包辦了。