生产者、消费者设计模式是多线程中经典的设计模式,也是面试常被问到的问题,本章我们将讨论多线程中生产者、消费者的问题。
.1)生产者、消费者问题描述
生产者、消费者设计模式,其实是一种“生产-消费-仓库--产品”模型,它应该具备以下几种特征:
(1)生产者只在仓库未满时生产产品,仓库满则停止生产;
(2)消费者只在仓库有产品余量时消费,仓库空则等待;
(3)当消费者发现仓库没有产品余量时通知生产者去生产;
(4)当生产者生产了产品后通知等待的消费者去消费。
.2)解决方案
(a)使用wait/notify/synchronized方法实现:
首先我们创建一个产品类:
//产品类
public class Product {
//产品编号
private int productId;
//产品名称
private String productName;
public int getProductId() {
return productId;
}
public void setProductId(int productId) {
this.productId = productId;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public Product(int productId, String productName) {
super();
this.productId = productId;
this.productName = productName;
}
}
这个类比较简单,主要创建一个产品类(产品编号、产品名称),我们再来创建一个仓库,进行产品的管理
//仓库类
public class WareHouse {
// 仓库容量
private int size;
LinkedList<Product> queue = new LinkedList<Product>();
public WareHouse(int size) {
this.size = size;
}
public void createProduct() {
try {
synchronized (queue) {
while (queue.size() == size) {
System.out.println("仓库已满");
System.out.println("Create_Thread[_"
+ Thread.currentThread().getId() + "_]" + "等待");
queue.wait();
}
int i = (int) Thread.currentThread().getId();
Product p = new Product(i, "Product__" + i);
queue.addFirst(p);
System.out.println("Create_Thread[_" + Thread.currentThread().getId()
+ "_]:生产了Product[" + i + "," + p.getProductName() + "],当前仓库大小为:"+queue.size());
queue.notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void consumeProduct() {
try {
synchronized (queue) {
while (queue.size() == 0) {
System.out.println("Consume_Thread[_"
+ Thread.currentThread().getId() + "_]" + "等待,当前仓库大小为:"+queue.size());
queue.wait();
}
System.out.println("Consume_Thread[_" + Thread.currentThread().getId()
+ "_]:消费了[" + queue.getFirst().getProductId() + ","
+ queue.getFirst().getProductName() + "],当前仓库大小为:"+(queue.size()-1));
queue.removeFirst();
queue.notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public LinkedList<Product> getQueue() {
return queue;
}
public void setQueue(LinkedList<Product> queue) {
this.queue = queue;
}
}
仓库,拥有容量属性,并创建一个LinkedList进行product的保存,实现了两个方法,进行产品的创建与消费,再看看我们的生产者、消费者类,
public class Producer extends Thread {
private WareHouse house;
public Producer(WareHouse house) {
this.house = house;
}
@Override
public void run() {
house.createProduct();
}
}
消费者:
public class Customer extends Thread {
private WareHouse house;
public Customer(WareHouse house) {
this.house = house;
}
@Override
public void run() {
house.consumeProduct();
}
}
测试类:
public class Test {
public static void main(String[] args) {
WareHouse house = new WareHouse(5);
for (int i = 0; i < 10; i++) {
Thread pro1 = new Producer(house);
Thread cus1 = new Customer(house);
cus1.start();
pro1.start();
}
}
}
来看看测试结果:
Consume_Thread[_9_]等待,当前仓库大小为:0
Consume_Thread[_11_]等待,当前仓库大小为:0
Create_Thread[_10_]:生产了Product[10,Product__10],当前仓库大小为:1
Create_Thread[_8_]:生产了Product[8,Product__8],当前仓库大小为:2
Consume_Thread[_17_]:消费了[8,Product__8],当前仓库大小为:1
Consume_Thread[_11_]:消费了[10,Product__10],当前仓库大小为:0
Consume_Thread[_19_]等待,当前仓库大小为:0
Consume_Thread[_9_]等待,当前仓库大小为:0
Create_Thread[_14_]:生产了Product[14,Product__14],当前仓库大小为:1
Consume_Thread[_15_]:消费了[14,Product__14],当前仓库大小为:0
Create_Thread[_12_]:生产了Product[12,Product__12],当前仓库大小为:1
Consume_Thread[_13_]:消费了[12,Product__12],当前仓库大小为:0
Create_Thread[_20_]:生产了Product[20,Product__20],当前仓库大小为:1
Consume_Thread[_23_]:消费了[20,Product__20],当前仓库大小为:0
Consume_Thread[_9_]等待,当前仓库大小为:0
Consume_Thread[_19_]等待,当前仓库大小为:0
Create_Thread[_18_]:生产了Product[18,Product__18],当前仓库大小为:1
Consume_Thread[_21_]:消费了[18,Product__18],当前仓库大小为:0
Create_Thread[_16_]:生产了Product[16,Product__16],当前仓库大小为:1
Create_Thread[_24_]:生产了Product[24,Product__24],当前仓库大小为:2
Consume_Thread[_19_]:消费了[24,Product__24],当前仓库大小为:1
Create_Thread[_26_]:生产了Product[26,Product__26],当前仓库大小为:2
Consume_Thread[_9_]:消费了[26,Product__26],当前仓库大小为:1
Consume_Thread[_25_]:消费了[16,Product__16],当前仓库大小为:0
Create_Thread[_22_]:生产了Product[22,Product__22],当前仓库大小为:1
Consume_Thread[_27_]:消费了[22,Product__22],当前仓库大小为:0
如上,一个简单的生产者、消费者模型就搭建完毕了,这种方式通过synchronized、wait、notify等方法控制并发,属于线程的基本实现方法,其实java中已经有现成的实现类实现生产者、消费者模式,下面我们来看一下。
(b)BlockingQueue 阻塞队列方式
我们来修改一下仓库类:
public class WareHouse2 {
// 仓库容量
private int size;
private LinkedBlockingQueue<Product> queue;
public WareHouse2(int size) {
this.size = size;
this.queue=new LinkedBlockingQueue<Product>(size);
}
public void createProduct() {
try {
int i = (int) Thread.currentThread().getId();
Product p = new Product(i, "Product__" + i);
queue.put(p);
System.out.println("Thread_create>:["+Thread.currentThread().getId()+"],queue_size:"+queue.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void consumeProduct() {
try {
Product pt=queue.take();
System.out.println("Thread_consume:["+Thread.currentThread().getId()+"],Product:["+pt.getProductId()+"]");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果:
Thread_create>:[8],queue_size:1
Thread_create>:[12],queue_size:2
Thread_create>:[16],queue_size:3
Thread_consume:[9],Product:[8]
Thread_consume:[11],Product:[12]
Thread_consume:[13],Product:[16]
Thread_create>:[10],queue_size:1
Thread_consume:[15],Product:[10]
Thread_consume:[19],Product:[14]
Thread_create>:[18],queue_size:1
Thread_create>:[20],queue_size:2
Thread_consume:[25],Product:[18]
Thread_create>:[24],queue_size:2
Thread_create>:[14],queue_size:2
Thread_consume:[17],Product:[20]
Thread_consume:[21],Product:[24]
Thread_consume:[23],Product:[22]
Thread_create>:[22],queue_size:0
Thread_consume:[27],Product:[26]
Thread_create>:[26],queue_size:0
通过源码,可以发现这种方式代码要简洁很多,而且在仓库类中,我们使用的put(),take()方法,当容量达到最大时,put会自动阻塞线程,take方法当容量为0时,也会自动阻塞线程,我们来看看源码
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
我们可以发现,在方法中自动封装了wait(),signal()等同步方法,所以就不需要我们程序自己去处理,从难易程度及安全性准确性来讲,建议大家使用阻塞队列实现生产者、消费者模式。