在很多书和博客里,阻塞队列和生产者消费者模式是绑在一起的,好像二者关系是绝对密不可分互为实现方案的,但事实上并不是,生产者消费者模式是多线程编程中常见的解决方案,阻塞队列则是实现生产者消费者方案的一种(较好的)实现方法。
我们就阻塞队列开始讲,然后在生产者和消费者模式的讲解中会给出几种常见的实现方案。
阻塞队列(BlockingQueue)是线程安全版本的队列,它支持线程阻塞。当我们向一个空的阻塞队列请求数据的时候,它会阻塞直至有新的数据插入;相对的,如果我们向一个全满的队列中插入数据,它也会阻塞知道有新的位置可供数据插入。
在阻塞队列中存在以下方法可供使用:
方法 | 正常动作 | 特殊处理 |
---|---|---|
add | 添加一个元素 | 如果队列满,抛出IllegalStatException异常 |
element | 返回队列的头元素 | 如果队列空,抛出NoSuchElementException异常 |
remove | 移除并返回队列的头元素 | 如果队列空,抛出NoSuchElementException异常 |
offer | 添加一个元素并返回true | 如果队列满,返回false |
peek | 返回队列的头元素 | 如果队列为空,返回null |
poll | 移除并返回队列的头元素 | 如果队列为空,返回null |
put | 添加一个元素到队列 | 如果队列满,则阻塞 |
take | 移除并返回队列的头元素 | 如果队列空,则阻塞 |
从上面的方法集合里可以看出,BlockingQueue是支持阻塞动作的。
在Java的java.util.concurrent包中,提供了六种Blocking的实现:
所谓生产者就是一个生产数据(指令)的线程,消费者就是一个消费数据(指令)的线程。生产者不断地产生数据,消费者不断地使用数据,像很多设计模式一样,为了二者的解耦,我们会生成一个缓冲区,这个缓冲区负责接收和推出数据,并且处理数据缺少或者溢出时的特殊情况。
为了形象化这个场景,我们可以设想一下饭店里吃饭做饭的场景:
顾客不断走进来需求新的食物,厨师们不断劳动生产食物。这就是生产者和消费者关系。一般情况下在饭点厨师的生产能力可能跟不上点餐的速度,就会有很多客户等待,这些客户里可能有人等的时间长,有人是熟客或者Vip(优先级高),情况就变得复杂起来。这时候,就出现了服务员(缓冲区),有了服务员,顾客和厨师就不用直接沟通了,厨师也不用记着谁先来谁后到,顾客也不用因为不晓得还要等多久换家店了。点菜上菜记录顺序和偏好都由服务员负责,整个事务场景就会一下子清晰起来。
生产者消费者模式作为一个具有普遍实用性的方案,具有以下的特点;
生产者与消费者模式有几种的实现方案:
复习一下wait()和notify()方法。
在生产者消费者模式中的作用:
package com.vivi.myTest.synchronizedTest.produceAndConsume;
import java.util.LinkedList;
/**
* Created by vivit on 2017/10/16.
*/
public class Storage {
// 仓库最大容量
private final int MAX_SIZE = 100;
//仓库的存储载体
private LinkedList
生产者:
package com.vivi.myTest.synchronizedTest.produceAndConsume;
/**
* Created by vivit on 2017/10/16.
*/
public class Producer extends Thread{
private int num;
// 所在放置的仓库
private Storage storage;
// 构造函数,设置仓库
public Producer(Storage storage) {
this.storage = storage;
}
// 线程run函数
public void run() {
produce(num);
}
// 调用仓库Storage的生产函数
public void produce(int num) {
storage.produce(num);
}
// get/set方法
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public Storage getStorage() {
return storage;
}
public void setStorage(Storage storage) {
this.storage = storage;
}
}
消费者
package com.vivi.myTest.synchronizedTest.produceAndConsume;
/**
* Created by vivit on 2017/10/16.
*/
public class Consumer extends Thread {
// 每次消费的产品数量
private int num;
// 所在放置的仓库
private Storage storage;
// 构造函数,设置仓库
public Consumer(Storage storage) {
this.storage = storage;
}
// 线程run函数
public void run() {
consume(num);
}
// 调用仓库Storage的生产函数
public void consume(int num) {
storage.consume(num);
}
// get/set方法
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public Storage getStorage() {
return storage;
}
public void setStorage(Storage storage) {
this.storage = storage;
}
}
测试入口:
package com.vivi.myTest.synchronizedTest.produceAndConsume;
/**
* Created by vivit on 2017/10/16.
*/
public class TestEntrance {
public static void main(String[] args) {
// 仓库对象
Storage storage = new Storage();
// 生产者对象
Producer p1 = new Producer(storage);
Producer p2 = new Producer(storage);
Producer p3 = new Producer(storage);
Producer p4 = new Producer(storage);
Producer p5 = new Producer(storage);
Producer p6 = new Producer(storage);
Producer p7 = new Producer(storage);
// 消费者对象
Consumer c1 = new Consumer(storage);
Consumer c2 = new Consumer(storage);
Consumer c3 = new Consumer(storage);
// 设置生产者产品生产数量
p1.setNum(10);
p2.setNum(10);
p3.setNum(10);
p4.setNum(10);
p5.setNum(10);
p6.setNum(10);
p7.setNum(80);
// 设置消费者产品消费数量
c1.setNum(50);
c2.setNum(20);
c3.setNum(30);
// 线程开始执行
c1.start();
c2.start();
c3.start();
p1.start();
p2.start();
p3.start();
p4.start();
p5.start();
p6.start();
p7.start();
}
}
打印输出:
【要消费的产品数量】:50/t【库存量】:0/t暂时不能执行生产任务!
【要消费的产品数量】:20/t【库存量】:0/t暂时不能执行生产任务!
【已经生产产品数】:10/t【现仓储量为】:10
【要消费的产品数量】:30/t【库存量】:10/t暂时不能执行生产任务!
【要消费的产品数量】:20/t【库存量】:10/t暂时不能执行生产任务!
【要消费的产品数量】:50/t【库存量】:10/t暂时不能执行生产任务!
【已经生产产品数】:10/t【现仓储量为】:20
【已经生产产品数】:10/t【现仓储量为】:30
【已经生产产品数】:10/t【现仓储量为】:40
【要消费的产品数量】:50/t【库存量】:40/t暂时不能执行生产任务!
【已经消费产品数】:20/t【现仓储量为】:20
【要消费的产品数量】:30/t【库存量】:20/t暂时不能执行生产任务!
【已经生产产品数】:80/t【现仓储量为】:100
【已经消费产品数】:50/t【现仓储量为】:50
【已经生产产品数】:10/t【现仓储量为】:60
【已经生产产品数】:10/t【现仓储量为】:70
【已经消费产品数】:30/t【现仓储量为】:40
因为缓冲区解耦了生产者和消费者的关系,当我们想要替换掉wait()/notify()方法时,只需要修改Storage即可,不需要修改生产者和消费者类。
代码如下:
package com.vivi.myTest.synchronizedTest.produceAndConsume;
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by vivit on 2017/10/16.
*/
public class StorageWithAwait {
// 仓库最大存储量
private final int MAX_SIZE = 100;
// 仓库存储的载体
private LinkedList list = new LinkedList();
// 锁
private final Lock lock = new ReentrantLock();
// 仓库满的条件变量
private final Condition full = lock.newCondition();
// 仓库空的条件变量
private final Condition empty = lock.newCondition();
// 生产num个产品
public void produce(int num)
{
// 获得锁
lock.lock();
// 如果仓库剩余容量不足
while (list.size() + num > MAX_SIZE)
{
System.out.println("【要生产的产品数量】:" + num + "/t【库存量】:" + list.size()
+ "/t暂时不能执行生产任务!");
try
{
// 由于条件不满足,生产阻塞
full.await();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
// 生产条件满足情况下,生产num个产品
for (int i = 1; i <= num; ++i)
{
list.add(new Object());
}
System.out.println("【已经生产产品数】:" + num + "/t【现仓储量为】:" + list.size());
// 唤醒其他所有线程
full.signalAll();
empty.signalAll();
// 释放锁
lock.unlock();
}
// 消费num个产品
public void consume(int num)
{
// 获得锁
lock.lock();
// 如果仓库存储量不足
while (list.size() < num)
{
System.out.println("【要消费的产品数量】:" + num + "/t【库存量】:" + list.size()
+ "/t暂时不能执行生产任务!");
try
{
// 由于条件不满足,消费阻塞
empty.await();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
// 消费条件满足情况下,消费num个产品
for (int i = 1; i <= num; ++i)
{
list.remove();
}
System.out.println("【已经消费产品数】:" + num + "/t【现仓储量为】:" + list.size());
// 唤醒其他所有线程
full.signalAll();
empty.signalAll();
// 释放锁
lock.unlock();
}
// set/get方法
public int getMAX_SIZE()
{
return MAX_SIZE;
}
public LinkedList getList()
{
return list;
}
public void setList(LinkedList list)
{
this.list = list;
}
}
打印输出
【要消费的产品数量】:50 【库存量】:0 暂时不能执行生产任务!
【要消费的产品数量】:30 【库存量】:0 暂时不能执行生产任务!
【已经生产产品数】:10 【现仓储量为】:10
【已经生产产品数】:10 【现仓储量为】:20
【要消费的产品数量】:50 【库存量】:20 暂时不能执行生产任务!
【要消费的产品数量】:30 【库存量】:20 暂时不能执行生产任务!
【已经生产产品数】:10 【现仓储量为】:30
【要消费的产品数量】:50 【库存量】:30 暂时不能执行生产任务!
【已经消费产品数】:20 【现仓储量为】:10
【已经生产产品数】:10 【现仓储量为】:20
【要消费的产品数量】:30 【库存量】:20 暂时不能执行生产任务!
【已经生产产品数】:80 【现仓储量为】:100
【要生产的产品数量】:10 【库存量】:100 暂时不能执行生产任务!
【已经消费产品数】:50 【现仓储量为】:50
【已经生产产品数】:10 【现仓储量为】:60
【已经消费产品数】:30 【现仓储量为】:30
【已经生产产品数】:10 【现仓储量为】:40
这里就是我们的重点部分了,就是使用阻塞队列实现生产者消费者模式。
根据第一部分对于阻塞队列的介绍,我们再次改写Storage类。
package com.vivi.myTest.synchronizedTest.produceAndConsume;
import java.util.concurrent.LinkedBlockingQueue;
/**
* Created by vivit on 2017/10/16.
*/
public class StorageWithBlockingQueue {
// 仓库最大存储量
private final int MAX_SIZE = 100;
// 仓库存储的载体
private LinkedBlockingQueue list = new LinkedBlockingQueue(
100);
// 生产num个产品
public void produce(int num)
{
// 如果仓库剩余容量为0
if (list.size() == MAX_SIZE)
{
System.out.println("【库存量】:" + MAX_SIZE + "/t暂时不能执行生产任务!");
}
// 生产条件满足情况下,生产num个产品
for (int i = 1; i <= num; ++i)
{
try
{
// 放入产品,自动阻塞
list.put(new Object());
}
catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println("【现仓储量为】:" + list.size());
}
}
// 消费num个产品
public void consume(int num)
{
// 如果仓库存储量不足
if (list.size() == 0)
{
System.out.println("【库存量】:0/t暂时不能执行生产任务!");
}
// 消费条件满足情况下,消费num个产品
for (int i = 1; i <= num; ++i)
{
try
{
// 消费产品,自动阻塞
list.take();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
System.out.println("【现仓储量为】:" + list.size());
}
// set/get方法
public LinkedBlockingQueue getList()
{
return list;
}
public void setList(LinkedBlockingQueue list)
{
this.list = list;
}
public int getMAX_SIZE()
{
return MAX_SIZE;
}
}
打印输出:
【库存量】:0 暂时不能执行生产任务!
【库存量】:0 暂时不能执行生产任务!
【现仓储量为】:1
【现仓储量为】:1
【现仓储量为】:3
【现仓储量为】:4
【现仓储量为】:5
【现仓储量为】:6
【现仓储量为】:7
【现仓储量为】:8
【现仓储量为】:9
【现仓储量为】:10
【现仓储量为】:11
【现仓储量为】:1
.....
注:文中代码引用了MONKEY_D_MENG作者的演示代码生产者/消费者问题的多种Java实现方式
到这里,阻塞线程和生产者消费者模式就都讲清楚了,自己也难得系统复习一下多线程的基础知识。下一篇文章会力争把线程池讲清楚。
相对于最开始的两篇文章,生产者和消费者模式开始,才开始真正有实践意义,前面的基础知识只能让我们同时多写几个“Hello world”。
碎碎念几下,我觉得线程池中这个“池”的概念真的很重要很基础,基本是一个程序员的必知必会内容。我陪boss面试面别人的时候,线程池数据库连接池各种资源池基本是必问内容。我自己工作时也确实体会到了池子的重要性,希望下一个文章能把池子的概念和线程池梳理清楚。
附上版权声明:
原作者:Vi_error,博客地址:Vi_error.nextval
转载请保持署名和注明原地址**