生产者-消费者模式是一个经典的多线程设计模式,它为多线程间的协作提供了良好的解决方案。生产者-消费者模式不在GOF的23种OO(Object Oriented,面向对象的)设计模式中,它是一种非OO的设计模式。
在生产者-消费者模式中,通常有两类线程:负责提交用户请求的生产者线程、负责具体处理生产者提交的请求(用户请求)的消费者线程。
在生产者-消费者模式中,生产者与消费者并不直接进行通信,而是生产者先将任务提交到共享缓冲区,然后消费者从共享缓冲区中获取任务并进行处理。
生产者-消费者模式的核心在于共享内存缓冲区。使用内存缓冲作为作为生产者和消费者之间通信的桥梁,避免了生产者和消费者直接通信,从而将生产者与消费者进行解耦。这里使用内存缓冲区是十分必要的。使用内存缓冲区,生产者无须等待消费者接收任务,直接将任务放入缓冲区即可开始下一个任务的”生产“,消费者也类似。这就使得生产者与消费者两者能够独立并发,缓解了生产者与消费者在执行速度上的时间差,支持忙闲不均的情况。
实现
这里先定义用于生产者和消费者之间交互的数据模型PCDada,后面的所有实现方式均使用该对象
/**
* Created by zhangcs on 17-4-16.
*/
public final class PCData {
private int intData;
public PCData( int intData){
this.intData = intData;
}
public int getIntData() {
return intData;
}
@Override
public String toString(){
return "data:" + intData;
}
}
1、使用synchronized锁以及wait()、notify()方法实现
使用wait()、notify()方法实现比较简单,首先是定义缓冲区,这里为了方便就使用List来实现;其次是定义缓冲区大小,当缓冲区已满或缓冲区为空时分别阻塞生产者和消费者
/**
* Created by zhangcs on 17-4-16.
* 生产者 将任务插入缓冲区
*/
public class Producer implements Runnable {
// 缓冲空间最大容量
private final int MAX_SIZE = 10;
// 计数器
// 因为int的自增操作不是线程安全的,所以这里使用AtomicInteger
private static AtomicInteger counter = new AtomicInteger();
private List<PCData> list;
public Producer( List<PCData> list){
this.list = list;
}
@Override
public void run() {
synchronized (list){
while ( list.size() > MAX_SIZE){
System.out.println( "缓冲区已满,生产者阻塞");
try{
// 由于条件不满足,生产阻塞
list.wait();
} catch( InterruptedException e){
e.printStackTrace();
}
}
// 创建任务,并加入缓冲区
PCData data = new PCData( counter.incrementAndGet());
list.add( data);
System.out.println( data + " 加入队列");
list.notifyAll();
}
}
public int getMAX_SIZE(){
return MAX_SIZE;
}
}
/**
* Created by zhangcs on 17-4-16.
* 消费者 从缓冲区获取任务并处理
*/
public class Consumer implements Runnable {
private List<PCData> list;
public Consumer( List<PCData> list){
this.list = list;
}
@Override
public void run() {
synchronized (list){
while( true){
while ( list.size() <= 0){
System.out.println("缓冲区没有任务,消费者阻塞");
try{
// 由于条件不满足,消费阻塞
list.wait();
} catch( InterruptedException e){
e.printStackTrace();
}
}
// 消费
PCData data = list.get( 0);
list.remove( 0);
System.out.println( "消费者消费" + data );
list.notifyAll();
}
}
}
}
在使用时将同一个缓冲区LinkedList分别传入生产者、消费者即可
@Test
public void test() throws InterruptedException {
// 缓冲区
List<PCData> list = new LinkedList<>();
// 创建多个生产者
Producer producer1 = new Producer( list);
Producer producer2 = new Producer( list);
Producer producer3 = new Producer( list);
Producer producer4 = new Producer( list);
// 创建消费者
Consumer consumer = new Consumer( list);
// 创建线程池,并执行生产者和消费者
ExecutorService service = Executors.newCachedThreadPool();
service.execute( producer1);
service.execute( producer2);
service.execute( producer3);
service.execute( producer4);
service.execute( consumer);
Thread.sleep( 10 * 1000);
service.shutdown();
}
2、使用Lock锁以及await()、signal()方法实现
使用await()、signal()方法实现可以实现生产者和消费者共用锁对象或者分别使用锁对象的效果,实现方式与上面的方法类似。这里以共用锁为例
/**
* Created by zhangcs on 17-4-17.
* 生产者 将任务插入缓冲区
*/
public class Producer implements Runnable {
// 缓冲区大小
private final int MAX_VALUE = 10;
// 载体/缓冲区
private List<PCData> list;
// 计数器
private static AtomicInteger counter = new AtomicInteger();
// 锁
private final Lock lock;
// 缓冲区满的条件变量
private final Condition full;
// 缓冲区空的条件变量
private final Condition empty;
// 这里传入锁,使得生产者、消费者共用同一个锁
public Producer( List<PCData> list, Lock lock, Condition full, Condition empty){
this.list = list;
this.lock = lock;
this.full = full;
this.empty = empty;
}
@Override
public void run() {
// 加锁
lock.lock();
while( list.size() >= MAX_VALUE){
System.out.println( "缓冲区已满,生产者阻塞");
try{
// 缓冲区已满,阻塞线程
full.await();
}catch( InterruptedException e){
e.printStackTrace();
}
}
// 生产
PCData data = new PCData( counter.incrementAndGet());
list.add( data);
System.out.println( data + " 加入队列");
full.signalAll();
empty.signalAll();
// 释放锁
lock.unlock();
}
}
/**
* Created by zhangcs on 17-4-17.
* 消费者 从缓冲区获取任务并处理
*/
public class Consumer implements Runnable {
private List<PCData> list;
// 锁
private final Lock lock;
// 缓冲区满的条件变量
private final Condition full;
// 缓冲区空的条件变量
private final Condition empty;
// 这里传入锁,使得生产者、消费者共用同一个锁
public Consumer( List<PCData> list, Lock lock, Condition full, Condition empty){
this.list = list;
this.lock = lock;
this.full = full;
this.empty = empty;
}
@Override
public void run() {
while( true){
// 加锁
lock.lock();
while( list.size() <= 0){
System.out.println("缓冲区没有任务,消费者阻塞");
try{
// 由于条件不满足,消费阻塞
empty.await();
} catch( InterruptedException e){
e.printStackTrace();
}
}
// 消费
PCData data = list.get( 0);
list.remove( 0);
System.out.println( "消费者消费" + data );
full.signalAll();
empty.signalAll();
// 释放锁
lock.unlock();
}
}
}
使用时传入共用的缓冲区和锁
@Test
public void test() throws InterruptedException {
// 创建缓冲区
List<PCData> list = new LinkedList<>();
// 创建锁
Lock lock = new ReentrantLock();
Condition full = lock.newCondition();
Condition empty = lock.newCondition();
// 创建生产者
Producer producer1 = new Producer( list, lock, full, empty);
Producer producer2 = new Producer( list, lock, full, empty);
Producer producer3 = new Producer( list, lock, full, empty);
// 创建消费者
Consumer consumer = new Consumer( list, lock, full, empty);
// 创建线程池,并执行生产者和消费者线程
ExecutorService service = Executors.newCachedThreadPool();
service.execute( producer1);
service.execute( producer2);
service.execute( producer3);
service.execute( consumer);
Thread.sleep( 10 * 1000);
service.shutdown();
}
3、使用阻塞队列BlockingQueue实现
常用的BlockingQueue具体实现类有五个,可以根据需求选择适用的实现类
实现类 | 说明 |
---|---|
ArrayBlockingQueue | 基于数组,生产者和消费者获取数据共用同一个锁对象,意味着两者无法真正并行运行。在创建时可以控制对象内部锁是否公平锁,默认非公平锁。 |
LinkedBlockingQuque | 基于链表,当内部数据缓存列表达到最大缓存容量时才会阻塞,生产者与消费者分别采用不同的锁来进行控制。 |
PriorityBlockingQueue | 基于优先级的阻塞队列,只在没有可消费的数据时阻塞消费者。注意:生产者生产数据的速度不能快于消费者的速度,否则时间一会耗尽所有可用堆内存空间。 |
DelayQueue | 只有当其指定的延迟时间到了才能够从队列中获取元素。没有大小限制,因此在往队列里插入数据的时候永远不会阻塞,只有获取数据的操作才会阻塞。 |
SynchronousQueue | 无缓冲的等待队列,内部没有数据缓冲空间(它的isEmpty()方法总是返回true),因此数据元素只有消费者获取的时候才可能存在。类似于现实生活中的一手交钱一手交货的情景,数据在配对的生产者和消费者之间直接传递,并不会将数据缓冲到数据队列中。SynchronousQueue一般用于线程池,如Executors.newCachedThreadPool()使用的就是SynchronousQueue。 |
对于每个BlockingQueue实现类的具体使用请自行参考API,这里直接上示例
/**
* Created by zhangcs on 17-4-16.
* 生产者 将任务插入缓冲区
*/
public class Producer implements Runnable {
private volatile boolean isRunning = true;
private BlockingQueue<PCData> queue;
// 因为int的自增操作不是原子的,所以这里使用AtomicInteger用于计数
private static AtomicInteger integer = new AtomicInteger();
public Producer( BlockingQueue<PCData> queues){
this.queue = queues;
}
@Override
public void run() {
PCData data = null;
try{
while ( isRunning) {
Thread.sleep( 500);
data = new PCData( integer.incrementAndGet());
System.out.println( data + " 加入队列");
if( !queue.offer( data, 2, TimeUnit.SECONDS)){
System.out.println( data + "加入失败");
}
}
}catch( InterruptedException ie){
ie.printStackTrace();
Thread.currentThread().interrupt();
}
}
public void stop(){
isRunning = false;
}
}
/**
* Created by zhangcs on 17-4-16.
* 消费者 从缓冲区获取任务并处理
*/
public class Consumer implements Runnable {
private BlockingQueue<PCData> queue;
public Consumer( BlockingQueue<PCData> queue){
this.queue = queue;
}
@Override
public void run() {
try{
while (true){
PCData data = queue.take();
if ( data != null){
System.out.println( "消费者消费" + data);
// 等待2000ms,模拟生产者与消费者执行速度存在差异的情况
Thread.sleep( 2 * 1000);
}
}
}catch( InterruptedException ie){
ie.printStackTrace();
Thread.currentThread().interrupt();
}
}
}
使用时创建缓冲区,并将缓冲区同时传递给生产者和消费者
public static void main( String[] args) throws InterruptedException {
// 建立缓冲区
BlockingQueue<PCData> queue = new LinkedBlockingQueue<>(10);
Producer producer1 = new Producer( queue); // 建立生产者
Producer producer2 = new Producer( queue);
Consumer consumer1 = new Consumer( queue); // 建立消费者
// 使用线程池运行生产者和消费者
ExecutorService service = Executors.newCachedThreadPool();
service.execute( producer1);
service.execute( producer2);
service.execute( consumer1);
Thread.sleep( 20 * 1000);
// 停止生产者
producer1.stop();
producer2.stop();
Thread.sleep( 13000);
service.shutdown();
}
4、管道方法
上面几种方法其实本质上是一样的,都是基于线程并发然后使用锁来同步。而管道不同,管道是基于进程的并发。使用管道无需担心线程安全、内存分配等问题,利于降低开发成本、调试成本。使用管道的方法优势明显,但是劣势也十分明显,它仅适用于生产者与消费者一对一的情况,而且无法进行跨机器通讯,在实际的场景中并不实用。
在使用管道进行传输的时候,对象需要进行序列化,所以PCData对象须要先实现Serializable接口
/**
* Created by zhangcs on 17-4-16.
* 生产者 将任务放入管道
*/
public class Producer implements Runnable{
// 管道输出流
private PipedOutputStream out;
public Producer( PipedOutputStream out){
this.out = out;
}
@Override
public void run() {
try{
// 序列化对象
PCData data = new PCData( 1);
ByteArrayOutputStream baops = new ByteArrayOutputStream();
new ObjectOutputStream( baops).writeObject( data);
byte[] da = baops.toByteArray();
// 生产者生产任务并传入管道
out.write( da);
System.out.println( data + "加入队列");
out.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
/**
* Created by zhangcs on 17-4-17.
* 消费者 从管道获取任务并处理
*/
public class Consumer implements Runnable {
// 管道输入流
private PipedInputStream in;
public Consumer( PipedInputStream in){
this.in = in;
}
@Override
public void run() {
ObjectInputStream oi = null;
try {
// 从管道中获取任务,并将获取到的任务反序列化并进行消费
byte[] bytes = new byte[1024];
int len = in.read( bytes);
oi = new ObjectInputStream( new ByteArrayInputStream( bytes)); // 反序列化
PCData data = (PCData) oi.readObject();
System.out.println( "消费者消费"+ data);
in.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
if( oi != null){
try {
oi.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
使用是需要将输入管道与输出管道相关联
@Test
public void test() throws InterruptedException{
// 创建并关联输入、输出管道
PipedInputStream in = new PipedInputStream();
PipedOutputStream out = new PipedOutputStream();
try{
out.connect( in);
}catch( IOException e){
e.printStackTrace();
}
// 创建生产者
Producer producer = new Producer( out);
// 创建消费者
Consumer consumer = new Consumer( in);
// 创建线程池,并允许生产者和消费者
ExecutorService service = Executors.newCachedThreadPool();
service.execute( producer);
service.execute( consumer);
Thread.sleep( 10 * 1000);
service.shutdown();
}