昨天去了一家游戏公司复试,这就是一道面试题目,要求用Java基础实现生产者消费者模式(机试),当时准确地说只完成了一半。开启两个线程时没什么问题,但后来面试官要求开启20个线程,结果就出现了假死。当时也没弄懂是什么原因导致假死,回来才弄懂!
1、什么是生产者消费者模式?
在实际的开发工作中,也会有这样的情节:某个模块负责生产数据(产品),而这些数据由另一个模块负责消费(此处的模块是广义的,可以是类、方法、线程、进程等)。在这里,负责生产数据的模块就是生产者,而负责处理这些数据的消费者,在生产者与消费者之间加一个数据的缓存区,生成着负责向其中放产品,而消费者从其中取产品,这样就构成了生产者消费者模式。如图:
2、生产者消费者模式的优点
(1)解耦
假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化,可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。
举个例子,我们去邮局投递信件,如果不使用邮筒(也就是缓冲区),你必须得把信直接交给邮递员。有同学会说,直接给邮递员不是挺简单的嘛?其实不简单,你必须得认识谁是邮递员,才能把信给他(光凭身上穿的制服,万一有人假冒,就惨了)。这就产生和你和邮递员之间的依赖(相当于生产者和消费者的强耦合)。万一哪天邮递员换人了,你还要重新认识一下(相当于消费者变化导致修改生产者代码)。而邮筒相对来说比较固定,你依赖它的成本就比较低(相当于和缓冲区之间的弱耦合)。
(2)支持并发
由于生产者与消费者是两个独立的并发体,他们之间是用缓冲区作为桥梁连接,生产者只需要往缓冲区里丢数据,就可以继续生产下一个数据,而消费者只需要从缓冲区了拿数据即可,这样就不会因为彼此的处理速度而发生阻塞。
接上面的例子,如果我们不使用邮筒,我们就得在邮局等邮递员,直到他回来,我们把信件交给他,这期间我们啥事儿都不能干(也就是生产者阻塞),或者邮递员得挨家挨户问,谁要寄信(相当于消费者轮询)。
(3)支持忙闲不均
缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来了。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。
为了充分复用,我们再拿寄信的例子来说事。假设邮递员一次只能带走1000封信。万一某次碰上情人节(也可能是圣诞节)送贺卡,需要寄出去的信超过1000封,这时候邮筒这个缓冲区就派上用场了。邮递员把来不及带走的信暂存在邮筒中,等下次过来时再拿走。
3、Java实现消费者生产者模式
(1)产品类
package com.producerconsumer.demo;
/**
*
* 产品类
* @author gerdan
*
*/
public class Product {
private String name;
public Product(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
(2)产品销售员
package com.producerconsumer.demo;
import java.util.ArrayList;
import java.util.List;
/**
* 销售员
* @author gerdan
*
*/
public class Clerk {
//货架对象,用于存储产品
private List<Product> store = new ArrayList<Product>();
//最多能存放数量
private static int MAX = 5;
/**
* 该方法由生产者调用
* 生产者把产品生产出来,然后调用该方法把产品交给售货员
* @param product 商品
*/
public synchronized void receiveProduct(Product product){
if(store.size()>=MAX){
try {
// 目前没有空间收产品,请稍候!
wait(); // 必须在同步块或方法中才能使用 wait
} catch (InterruptedException e) {
e.printStackTrace();
}
}
store.add(product);
System.out.printf("-->库存状态(%d) 新产品(%s)%n", store.size(), product.getName());
//通知等待区中的一个线程可以工作了
notify();
}
public synchronized Product buyProduct(){
if(store.size()==0){
try {
//目前货架上没有商品,需要等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Product p = store.remove(0);
System.out.printf("<--库存状态(%d) 取走产品(%s)%n", store.size(), p.getName());
// 通知等待区中的一个生产者可以继续工作了
notify(); // notify 和 interrupt 会使等待区域的线程回到锁定池的 Blocked 状态
return p;
}
}
(3)生产者
package com.producerconsumer.demo;
/**
* 生产者
*
* @author gerdan
*
*/
public class Producer implements Runnable {
Clerk clerk = new Clerk();
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
try {
// 暂停随机时间
Thread.sleep((int) Math.random() * 3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Product p = new Product(Thread.currentThread().getName()+"产品" + i);
//产品放入到货架中
clerk.receiveProduct(p);
}
}
}
(4)消费者类
package com.producerconsumer.demo;
/**
* 消费者
* @author gerdan
*
*/
public class Consumer implements Runnable {
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
//暂停若干时间
Thread.sleep((int) Math.random() * 5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//消费者取出商品
clerk.buyProduct();
}
}
}
(5)测试类
package com.producerconsumer.demo;
public class AppTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer producer = new Producer(clerk);
Consumer consumer = new Consumer(clerk);
new Thread(producer,"GARDAN").start();
new Thread(consumer).start();
/* for (int i = 0; i < 10; i++) {
new Thread(producer).start();
new Thread(consumer).start();
}*/
}
}
以上代码是我在机试时候写,当时面试官说让我开启10个线程试试,开启10个生产者和消费者线程,如测试类中注释的部分。结果运行,就报错了!
当时面试官就问我原因,一时间我也傻眼了。完全没头绪~~
后来回到家,查阅了一些资料才弄明白。故此需要mark下。
出错原因在于销售员类中,等待池有既有生产者线程也有消费者线程,在notify()调用时,无法确定唤醒的是生产者线程还是消费者线程,也就是说,当货架已满时,当前生产者线程wait()了,然后再调用notify(),而调用notify()是唤醒的生产者还是消费者是无法确定的,如果唤醒的是生产者,显然再放入产品就会越界了。而相同的消费者取出商品时也是同一种情况。
找到原因后就要对症下药,解决方案:
方案一:
修改销售员类,如下:
package com.producerconsumer.demo;
import java.util.ArrayList;
import java.util.List;
/**
* 销售员
* @author gerdan
*
*/
public class Clerk {
//货架对象,用于存储产品
private List<Product> store = new ArrayList<Product>();
//最多能存放数量
private static int MAX = 5;
/**
* 该方法由生产者调用
* 生产者把产品生产出来,然后调用该方法把产品交给售货员
* @param product 商品
*/
public synchronized void receiveProduct(Product product){
//使用while循环,判断货架是否已放满,如果被放满了就等待
while(store.size()>=MAX){
try {
// 目前没有空间收产品,请稍候!
wait(); // 必须在同步块或方法中才能使用 wait
} catch (InterruptedException e) {
e.printStackTrace();
}
}
store.add(product);
System.out.printf("-->库存状态(%d) 新产品(%s)%n", store.size(), product.getName());
//唤醒等待区中的全部线程
notifyAll();
}
public synchronized Product buyProduct(){
//使用while循环,判断货架中是否有产品,如果没有就继续等待
while(store.size()==0){
try {
//目前货架上没有商品,需要等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Product p = store.remove(0);
System.out.printf("<--库存状态(%d) 取走产品(%s)%n", store.size(), p.getName());
// //唤醒等待区中的全部线程
notifyAll();
return p;
}
}
首先,修改receiveProduct()和buyProduct()中的notify()为notifyAll()方法,唤醒等待池中全部线程。
然后,特别将等待条件有if判断改为while循环,以保证没有未满足条件时就持续等待。
以上解决方案性能比较低。第二种解决方案就是使用阻塞队列,队列是非常适合用于线程间的通讯。
方案二:
使用BlockingQueue阻塞队列来实现,当产品放满货架时BlockingQueue就会阻塞,当产品被取完也会进入阻塞。
package com.producerconsumer.demo;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ProducerConsumerOptimize {
public static void main(String[] args) {
BlockingQueue<String> store = new ArrayBlockingQueue<String>(5);
ProducerQueue producer = new ProducerQueue(store);
ConsumerQueue consumer = new ConsumerQueue(store);
for (int i = 1; i <= 10; i++) {
new Thread(producer, "生产者" + i).start();
new Thread(consumer, "消费者" + i).start();
}
}
}
/**
* 生产者:产出产品放入到队列queue中
*
* @author gerdan
*
*/
class ProducerQueue implements Runnable {
BlockingQueue<String> queue;
public ProducerQueue(BlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
// 停顿任意时间
Thread.sleep((int) Math.random() * 3000);
// 放入产品到队列中
System.out.println(Thread.currentThread().getName() + ": 产出了产品" + i);
queue.put(Thread.currentThread().getName() + ": 产品" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 消费者:从阻塞队列中消费产品
*
* @author gerdan
*
*/
class ConsumerQueue implements Runnable {
BlockingQueue<String> queue;
public ConsumerQueue(BlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
// 停顿任意时间
Thread.sleep((int) Math.random() * 5000);
// 消费者消费产品
System.out.println(Thread.currentThread().getName() + ": 消费"
+ queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}