在上一篇博客中的“生产者-消费者模式”中,生产者线程和消费者线程之间的共享数据区域是我们自己实现的一个MyBlockingQueue类,其实我们可以使用java.util.concurrent包下的BlockingQueue接口。
BlockingQueue接口在使用时需要制定其存储的元素的数据类型,它继承了Queue接口,因此它是一个队列,具有队列的性质。同时,在Java提供的API中有以下几种实现类:ArrayBlockingQueue、DelayQueue、LinkedBlockingDeque、 LinkedBlockingQueue、 PriorityBlockingQueue和SynchronousQueue。
BlockingQueue
BlockingQueue是一个支持两个附加操作的 Queue,这两个操作是:获取元素时等待队列变为非空,以及存储元素时等待空间变得可用。
在BlockingQueue中,以四种方式来实现这两个操作的:add(e)、remove()、element();offer()、poll()、peek();put(e)、take();offer(e, time, unit)、poll(time, unit)。对于不能立即满足但可能在将来某一时刻可以满足的操作,这四种形式的处理方式不同:第一种是抛出一个异常,第二种是返回一个特殊值(null 或 false,具体取决于操作),第三种是在操作可以成功前,无限期地阻塞当前线程,第四种是在放弃前只在给定的最大时间限制内阻塞。下表中总结了这些方法:
BlockingQueue作为一个队列,它不接受 null 元素。当试图 add、put 或 offer 一个 null 元素时,某些实现会抛出 NullPointerException。Null在BlockingQueue中被用作指示poll操作失败的警戒值。BlockingQueue 可以是限定容量的。它在任意给定时间都可以有一个 remainingCapacity,超出此容量,便无法无阻塞地 put 附加元素。
BlockingQueue实现主要用于生产者-使用者队列,BlockingQueue实现是线程安全的。所有排队方法都可以使用内部锁或其他形式的并发控制来自动达到它们的目的。
ArrayBlockingQueue
它继承了AbstractQueue抽象类,实现了BlockingQueue, Serializable接口。
一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部 是在队列中存在时间最长的元素。队列的尾部 是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。
这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。
LinkedBlockingQueue
一个基于已链接节点的、范围任意的 blocking queue。此队列按 FIFO(先进先出)排序元素。队列的头部 是在队列中时间最长的元素。队列的尾部 是在队列中时间最短的元素。新元素插入到队列的尾部,并且队列获取操作会获得位于队列头部的元素。链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。
注意:1、链接队列的吞吐量通常高于数组的队列,是因为链接队列的插入和删除的速度要优于数组的队列。
2、LinkedBlockingQueue内部分别使用了takeLock 和 putLock 对并发进行控制,也就是说,添加和删除操作并不是互斥操作,可以同时进行,这样也就可以大大提高吞吐量。
3、由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。
可选的容量范围构造方法参数作为防止队列过度扩展的一种方法。如果未指定容量,则它等于 Integer.MAX_VALUE。除非插入节点会使队列超出容量,否则每次插入后会动态地创建链接节点。
队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
LinkedBlockingDeque
一个基于已链接节点的、任选范围的阻塞双端队列。
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。
相比于其他阻塞队列,LinkedBlockingDeque多了addFirst、addLast、peekFirst、peekLast等方法,以first结尾的方法,表示插入、获取获移除双端队列的第一个元素。以last结尾的方法,表示插入、获取获移除双端队列的最后一个元素。
可选的容量范围构造方法参数是一种防止过度膨胀的方式。如果未指定容量,那么容量将等于 Integer.MAX_VALUE。只要插入元素不会使双端队列超出容量,每次插入后都将动态地创建链接节点。
DelayQueue
DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。
该队列的头部 是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且 poll 将返回 null。当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于等于 0 的值时,将发生到期。即使无法使用 take 或 poll 移除未到期的元素,也不会将这些元素作为正常元素对待。
PriorityBlockingQueue
PriorityBlockingQueue是一个支持优先级的无界阻塞队列,它使用与类 PriorityQueue 相同的顺序规则,默认情况下采取自然顺序升序排列,并且提供了阻塞获取操作。
SynchronousQueue
它是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。
生产者-消费者模式
我们可以将上一篇博客中的生产者-消费者模式的代码中的共享数据区域改成阻塞队列,如改成ArrayBlockingQueue:
生产者线程:
import java.util.concurrent.BlockingQueue;
public class ProducerThread implements Runnable {
private BlockingQueue<Integer> bq;
public ProducerThread(BlockingQueue<Integer> bq) {
this.bq = bq;
}
@Override
public void run() {
for (int i = 0;i < 5;i++) {
try {
System.out.println("生产了" + i);
bq.put(i);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
消费者线程:
import java.util.concurrent.BlockingQueue;
public class ConsumerThread implements Runnable {
private BlockingQueue<Integer> bq;
public ConsumerThread(BlockingQueue<Integer> bq) {
this.bq = bq;
}
@Override
public void run() {
for (int i = 0;i < 5;i++) {
try {
System.out.println("消耗了" + i);
bq.take();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
测试代码:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class Cashier {
public static void main(String[] args) {
BlockingQueue<Integer> bq = new ArrayBlockingQueue<Integer>(5);
for (int i = 0;i < 5;i++) {
new Thread(new ProducerThread(bq)).start();
new Thread(new ConsumerThread(bq)).start();
}
}
}
运行结果: