浅析生产者消费者模式--多线程假死

昨天去了一家游戏公司复试,这就是一道面试题目,要求用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();
			}
		}
	}
}






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章