最近在学习《JAVA程序员修炼之道》的时候,顺带重读了JAVA之母的那本《JAVA并发编程实战》。在此,对concurrent包中的BlockingQueue的几种常见用法做一个小结,以备用时查阅。首先,我们回顾一下阻塞队列和它的常见用途——生产者消费者模式的基本知识。
什么是生产者消费者模式
生产者-消费者模式是一种将“找出需要完成的工作”与“执行工作”两个过程分开的设计模式,生产者负责把准备好的工作项放入一个“待完成”列表以备后续处理,而消费者负责从该列表中取出工作项并执行工作。它的好处在于,生产者与消费者彼此不关心对方的状态,数量甚至实现方式,从而将两者的代码解耦;另一方面,该模式简化了负载管理,使得开发者更易于处理生产者与消费者之间速率的差异。
什么是阻塞队列
阻塞队列,顾名思义,首先它是一个队列,可以使得数据由队列的一端输入,从另外一端输出。如下图所示:

而阻塞队列的不同之处在于,提供了阻塞的入队方法和阻塞的出队方法。即,如果队列满了,则入队方法将一直阻塞直到有空间可用;如果队列空了,则出队方法一直阻塞到有元素可用。如下图所示:
从上图不难发现,阻塞队列是生产者-消费者模式的天生支持者,利用这座桥梁,可以很轻松的在程序中实现这个模式。
复习完上述基本概念以后,我们开始进入今天的正题——JDK1.5引入的阻塞队列java.util.concurrent.BlockingQueue
BlockingQueue
BlockingQueue是阻塞队列的JAVA版,它提供了可阻塞的put和take方法,以及可定时(也可立即中止)的offer和poll方法。
这里要注意一点,队列存在有界队列与无界队列之分,无界队列永远不会填满,所以无界队列上的put方法永远不会阻塞,在实际应用中要考虑内存占用的问题。其接口如下所示:
add 增加一个元索 如果队列已满,则抛出一个IIIegaISlabEepeplian异常
remove 移除并返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
element 返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
offer 添加一个元素并返回true 如果队列已满,则返回false
poll 移除并返问队列头部的元素 如果队列为空,则返回null
peek 返回队列头部的元素 如果队列为空,则返回null
put 添加一个元素 如果队列满,则阻塞
take 移除并返回队列头部的元素 如果队列为空,则阻塞
BlockingQueue与concurrent包中的很多组件(如ScheduledExecutorService,CompletionService等)可以配合使用,起到事半功倍的效果。同时concurrent包的很多组件源码中也依赖于BlockingQueue,例如我们熟知的一系列线程池框架接口Executor的实现者们。
BlockingQueue的常见用法
这里,我总结了BlockingQueue的几种典型用法与用途:
1、经典型
/**
* <p/>
* <br>==========================
* <br> 开发:sunli
* <br> 版本:1.0
* <br> 创建时间:2013-11-16
* <br>==========================
*/
public class TestBlockingQueue {
/**
* 场景:小镇的火车站里源源不断的走出乘客,出站口有一个出租车等候点,乘客们在这里排队等候出租车。
* 小镇上一共有3辆出租车轮流的接送乘客,每辆出租车接到一位乘客,就需要花费一段时间将乘客送到目的地,并在此返回继续接其他乘客
*
* 1、每个人都需要排队等候,新来的排队者加入到队伍的末尾
* 2、火车站是生产者,不断生产出新的乘客
* 3、出租车是消费者,不断消费乘客,但每辆出租车在同一时段只能消费一个人
*/
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newCachedThreadPool();
//初始化一个排队队列,能够容纳10个人
BlockingQueue<Person> queue = new ArrayBlockingQueue<Person>(10);
Station station = new Station(queue);
Texi texi1 = new Texi(queue,"texi1");
Texi texi2 = new Texi(queue,"texi2");
Texi texi3 = new Texi(queue,"texi3");
service.execute(station);
service.execute(texi1);
service.execute(texi2);
service.execute(texi3);
Thread.sleep(3000);
service.shutdownNow();
}
//定义排队的人
static class Person {
private String name;
public Person(String name) {
this.name = name;
System.out.println(name + "被创建");
}
//上车出发
public void setOff() {
System.out.println("Nice,I will set off,I am" + name);
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}
//定义生产者,假定只有一个生产者
static class Station implements Runnable {
private BlockingQueue<Person> queue;
volatile static int count = 0;
public Station(BlockingQueue<Person> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (true) {
//生产人
System.out.println("火车站准备生产人: " + System.currentTimeMillis());
Person person = new Person("SB" + count++ + "号");
queue.put(person);
System.out.println("火车站生产人完毕: " + System.currentTimeMillis());
//休眠300ms
Thread.sleep(300);
}
} catch (InterruptedException ex) {
}
}
}
//定义一个消费者
static class Texi implements Runnable {
private BlockingQueue<Person> queue;
private String name;
public Texi(BlockingQueue<Person> queue, String s) {
this.name = s;
this.queue = queue;
}
public void run() {
try {
while (true) {
//消费乘客
System.out.println("消费者" + name + "准备消费乘客: " + System.currentTimeMillis());
Person person = queue.take();
//Yahoo,出发喽!!
person.setOff();
System.out.println("消费者" + name + "消费乘客完毕: " + "|||||||" + System.currentTimeMillis());
//休眠1000ms,才能接下一个客
Thread.sleep(1000);
}
} catch (InterruptedException ex) {
}
}
}
}
上述代码中,有一个火车站作为生产者,三个出租车作为消费者。火车站每生产一个乘客需要300ms休息才能生产下一个乘客,出租车每消费一个乘客需要1000ms休息才能消费下一个乘客。这是使用BlockingQueue的最简单直接的方式,但是如果借用concurrent包的其它组件,我们可以更轻易而高效的使用它。
2、与ScheduledExecutorService结合
/**
* <p/>
* <br>==========================
* <br> 开发:sunli
* <br> 版本:1.0
* <br> 创建时间:2013-11-16
* <br>==========================
*/
public class TestBlockingQueue1 {
/**
* 场景:出站口的另一端,还有一个公交站,因为小镇经济水平较高,来坐公交的人并不多。公交会
* 隔较长时间来一次公交站,并接走这里等候的全部乘客,在此之前,乘客们需要在公交站排队
* <p/>
* 1、每个人都需要排队等候,新来的排队者加入到队伍的末尾
* 2、火车站是生产者,不断生产出新的乘客
* 3、公交车是消费者,不断消费乘客,但每辆公交车在同一时段消费多个人
*/
public static void main(String[] args) throws InterruptedException {
ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
//初始化一个排队队列,能够容纳10个人
BlockingQueue<Person> queue = new ArrayBlockingQueue<Person>(10);
Station station = new Station(queue);
Bus bus = new Bus(queue,"公交1号");
Bus bus2 = new Bus(queue,"公交2号");
service.execute(station);
service.scheduleAtFixedRate(bus, 5, 10, TimeUnit.SECONDS);
service.scheduleAtFixedRate(bus2, 3, 10, TimeUnit.SECONDS);
Thread.sleep(30000);
service.shutdownNow();
}
//定义排队的人
static class Person {
private String name;
public Person(String name) {
this.name = name;
System.out.println(name + "被创建");
}
//上车出发
public void getOn() {
System.out.println("我上车了,哈哈,我是" + name);
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}
//定义生产者,假定只有一个生产者
static class Station implements Runnable {
private BlockingQueue<Person> queue;
volatile static int count = 0;
public Station(BlockingQueue<Person> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (true) {
//生产人
System.out.println("火车站准备生产人: " + System.currentTimeMillis());
Person person = new Person("SB" + count++ + "号");
queue.put(person);
System.out.println("火车站生产人完毕: " + System.currentTimeMillis());
//休眠300ms
Thread.sleep(300);
}
} catch (InterruptedException ex) {
}
}
}
//定义一个消费者
static class Bus implements Runnable {
private final String name;
private BlockingQueue<Person> queue;
public Bus(BlockingQueue<Person> queue,String name) {
this.queue = queue;
this.name=name;
}
public void run() {
try {
System.out.println("公交车"+name+"来了");
while (!queue.isEmpty()) {
//消费乘客
System.out.println("公交车"+name+"准备上乘客");
Person person = queue.poll();
if (person != null) {
//Yahoo,上车喽!!
person.getOn();
System.out.println("公交车"+name+"上了一位乘客");
//休眠100ms,才能再上下一位乘客
Thread.sleep(100);
}
}
System.out.println("公交车"+name+"开走了");
} catch (InterruptedException ex) {
}
}
}
}
在这里,情况有了不同,仍然还是一个火车站作为生产者,消费者变成了两辆公交车。两个消费者每隔10秒才启动一次,并消费完所有生产者生产的内容。这里有个注意点——定时启动的任务执行框架
ScheduledExecutorService。它具有延迟启动线程的方法schedule和按一定频率定时启动线程的方法scheduleAtFixedRate,因此可以简单实现“按一定频率去检查阻塞队列的任务,并执行相应任务”的目的。对于生产者生产数量不大,且实时性要求不高的系统,可以采用上述方案,每隔一个较长的时间10s启动一次消费者,消费者负责处理完阻塞队列中的所有任务;对于生产者数量不大,实时性要求较高,或者每个任务执行时间较长的系统,也可以采用下面改进做法:
public void run() {
Object obj = queue.poll();
if (obj != null) {
//业务流程
}
}
即消费者线程以一个较高的频率启动,比如每秒启动一个。每次启动都从阻塞队列中非阻塞的获取一个任务,如果任务不为空,则执行;否则什么也不做。