java实现多线程生产者消费者模式
1、概念
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
在实际生活中,老师和学生的关系就是一种生产者消费者模型,老师负责布置作业(生产),学生负责写作业(消费)。还有餐馆厨师负责做饭(生产),顾客负责吃饭(消费)等等。
2、参与对象
生产者:负责向缓冲区存放数据
消费者:负责从缓冲区读取数据
缓冲区:负责存放数据
3、优点
3.1、解耦
假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化,可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。
3.2、支持并发
生产者直接调用消费者的某个方法,还有另一个弊端。由于函数调用是同步的(或者叫阻塞的),在消费者的方法没有返回之前,生产者只好一直等在那边。万一消费者处理数据很慢,生产者就会白白糟蹋大好时光。
使用了生产者/消费者模式之后,生产者和消费者可以是两个独立的并发主体(常见并发类型有进程和线程两种,后面的帖子会讲两种并发类型下的应用)。生产者把制造出来的数据往缓冲区一丢,就可以再去生产下一个数据。基本上不用依赖消费者的处理速度。
其实当初这个模式,主要就是用来处理并发问题的。
3.3、支持忙闲不均
缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来了。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。
4、代码实现
说明:例子模拟生产者(Producer)消费者(Consumer)模式,其中缓冲区(Bean),当缓冲区数据达到10(FULL),生产者停止生产数据,当缓冲区没有数据,消费者停止消费数据。
4.1、Bean
public class Bean { private Integer count; public Integer getCount() { return count; } public void setCount(Integer count) { this.count = count; } }
4.2、生产者
public class Producer implements Runnable {private Bean bean; private static final Integer FULL = 10; public Producer(Bean bean) { this.bean = bean; } private void produce() throws InterruptedException { while (true){ Thread.sleep(1000); synchronized (bean) { if (bean.getCount() >= FULL) { System.out.println("生产者等待"); bean.wait(); } else { bean.setCount(bean.getCount() + 1); System.out.println(Thread.currentThread().getName() + "生产, current :" + bean.getCount()); bean.notifyAll(); } } } } @Override public void run() { try { produce(); } catch (InterruptedException e) { e.printStackTrace(); } } }
4.3、消费者
public class Consumer implements Runnable { private Bean bean; private static final Integer NULL = 0; public Consumer(Bean bean) { this.bean = bean; } private void consume() throws InterruptedException { while (true) { Thread.sleep(2000); synchronized (bean) { if (bean.getCount() <= NULL) { System.out.println("消费者等待"); bean.wait(); } else { bean.setCount(bean.getCount() - 1); System.out.println(Thread.currentThread().getName() + "消费, current :" + bean.getCount()); bean.notifyAll(); } } } } @Override public void run() { try { consume(); } catch (Exception e) { e.printStackTrace(); } } }
4.4、main
public class Test { public static void main(String[] args) { Bean bean = new Bean(); bean.setCount(0); for (int i = 0; i < 5; i++) { new Thread(new Producer(bean)).start(); new Thread(new Consumer(bean)).start(); } } }
4.5、输出
Thread-0生产, current :1 Thread-8生产, current :2 Thread-2生产, current :3 Thread-6生产, current :4 Thread-4生产, current :5 Thread-2生产, current :6 Thread-7消费, current :5 Thread-9消费, current :4 Thread-0生产, current :5 Thread-3消费, current :4 Thread-1消费, current :3 Thread-8生产, current :4 Thread-6生产, current :5 Thread-5消费, current :4 Thread-4生产, current :5 Thread-8生产, current :6 Thread-2生产, current :7 Thread-0生产, current :8 Thread-4生产, current :9 Thread-6生产, current :10 生产者等待 生产者等待
为什么要在count外面套一层Bean?不直接使用count呢
原因是缓冲区(count)需要对生产者消费者共享,synchronize不能锁Integer对象(操作Integer对象的时候,自动装箱/拆箱对象就已经变了,会失去同步的效果)
5、总结
以上是通过wait()/notifyAll()实现的生产者消费者模式,也是比较简单的一种实现方式,除此之外还有通过await() / signal()方法、BlockingQueue阻塞队列方法、信号量、管道等方法实现。