本篇文章介绍阻塞队列和生产者-消费者模式,部分内容总结摘抄自《Java并发编程的艺术》,仅作笔记。
阻塞队列介绍
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。
- 支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满。
- 支持阻塞的移除方法:在队列为空时,获取元素的线程会等待队列变为非空。
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。 阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。
在阻塞队列不可用(对生产者线程来说不可用=队列满,对消费者线程来说不可用=队列空)时,这两个附加操作提供了4中处理方式,如下表。
方法类型 | 抛出异常的方法 | 返回特殊值的方法 | 一直阻塞的方法 | 超时退出的方法 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
检查方法 | element() | peek() | 没有 | 没有 |
- 抛出异常:当队列满时,再往队列里插入元素会抛出IllegalStateException(“Queue full”)异常;当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
- 返回特殊值:当往队列里插入元素时,会返回元素是否插入成功的标志,成功则返回true;当从队列里取出元素时,如果队列为空则返回null。
- 一直阻塞:当阻塞队列满时,如果生产者线程调用put()方法向队列中添加元素,队列会一直阻塞生产者线程,直到队列不满或响应中断退出;当队列空时,如果消费者线程使用take()方法从队列里获取元素,队列会阻塞消费者线程,直到队列不为空。
- 超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出;当队列为空时,如果消费者线程从队列里获取元素,队列会阻塞消费者线程一段时间,如果超过了指定的时间,消费者线程就会退出。
Java里的阻塞队列
JDK7提供了7个阻塞队列,他们有共同的父接口BlockingQueue,BlockingQueue提供了以下方法。
//将指定元素插入到队列中,如果队列是满的,则抛出IllegalStateException异常
boolean add(E e);
//将指定元素插入到队列中,插入成功返回true,失败则返回false
boolean offer(E e);
//将指定元素插入到队列中,如果队列是满的,则一直阻塞直到队列不满或者响应中断退出
void put(E e) throws InterruptedException;
//将指定元素插入到队列中,如果队列是满的,则阻塞指定时间,超过该时间线程就会退出
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
//移除队列首元素,如果队列为空,则一直阻塞直到队列有元素
E take() throws InterruptedException;
//移除队列首元素,如果队列为空,则阻塞指定时间,超过该时间线程就会退出
E poll(long timeout, TimeUnit unit) throws InterruptedException;
//删除队列中的指定元素,如果没有此元素或队列为空,则抛出NoSuchElementException异常
boolean remove(Object o);
//如果队列中包含指定元素,则返回true,否则返回false
public boolean contains(Object o);
//删除队列中所有元素,并将它们添加到给定的集合中
int drainTo(Collection<? super E> c);
ArrayBlockingQueue
ArrayBlockingQueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。ArrayBlockingQueue支持线程公平的访问队列,但默认情况下是非公平的。为了为了保证公平性,通常会降低吞吐量。
LinkedBlockingQueue
LinkedBlockingQueue是一个用链表实现的有界阻塞队列,此队列的默认和最大长度为Inter.MAX_VALUE,按照先进先出的原则对元素进行排序。
PriorityBlockingQueue
PriorityBlockingQueue是一个支持优先级的无界阻塞队列,默认情况下元素采取自然顺序升序排列,也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化时PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。但不能保证相同优先级的元素的顺序。
DelayQueue
DelayQueue时一个支持延迟获取元素的无界阻塞队列,使用PriorityQueue实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素,只有在延迟期满时才能从队列中获取元素。
可以将DelayQueue使用在以下场景:
- 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
- 定时任务调度:使用DelayQueue保存当天将会执行的任何和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如TimerQueue就是使用DelayQueue实现的。
实现Delayed接口可以参考ScheduledThreadPoolExecutor里ScheduledFutureTask类的实现,可以分为三步。
- 在构造函数中初始化基本数据。使用time记录当前对象延迟到什么时候可以使用,使用sequenceNumber来标识元素在队列中的先后顺序。
- 实现getDelay方法。该方法返回当前元素还需要延时多长时间,单位是纳秒。
- 实现compareTo方法。该方法可以指定元素的顺序,例如可以让延时时间最长的放在队列的末尾。
延时阻塞队列的实现很简单,当消费者线程从队列里获取元素时,如果元素没有达到延期时间,就阻塞消费者线程。
SynchronousQueue
SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。支持公平访问队列,但默认情况采用非公平策略。队列本身不存储任何元素,非常适合传递性场景,例如把生产者线程处理的数据直接传递给消费者线程。
LinkedTransferQueue
LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多个tryTransfer和transfer方法。
- transfer方法:如果当前有消费者正在等待接收元素,transfer方法可以把生产者传入的元素立刻transfer给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者取出了才返回。
- tryTransfer方法:用来试探生产者传入的元素能否直接传给消费者。如果没有消费者等待接收元素,则返回false。与transfer方法的区别是tryTransfer方法无论消费者是否接收,方法都会立即返回,而transfer方法必须等到消费者消费了才返回。
LinkedBlockingDeque
LinkedBlockingDeque时一个由链表结构组成的双向阻塞队列,所谓双向队列指的是可以从队列的两端插入和移出元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。和其他的阻塞队列相比,LinkedBlockingDeque多了在队列首部和队列尾部插入和移出元素的方法。
阻塞队列实现原理
阻塞队列是使用通知模式实现的,即当生产者往满的队列里添加元素时会阻塞生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用;当消费者从一个空的队列中消费元素时会阻塞消费者,在生产者向队列中添加元素后,会通知消费者队列中有可用元素。
以上介绍的阻塞队列都是通过Lock和Condition实现的,有关这两个类在这篇文章详细介绍,此处不再赘述。
生产者-消费者模式
生产者和消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信,而是通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列。而消费者不直接与生产者要数据,而是从阻塞队列里取。阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
生产者-消费者模式有多种实现方法,包括使用Synchronized实现、使用Lock和Condition实现、阻塞队列实现、信号量实现和管道实现。
使用Synchronized实现
使用Synchronized实现就需要搭配wait()和notifyAll()方法。当队列满时,使全部生产者线程等待,直到消费者线程消费并通知后,生产者线程再继续插入元素;当队列空时,使全部消费者线程等待,直到生产者线程插入元素并通知后,消费者线程可以继续消费元素。
public class SynchronizedProAndCon {
public static void main(String[] args) {
Storage storage = new Storage();
Produce p1 = new Produce(storage,"p1");
Produce p2 = new Produce(storage,"p2");
Produce p3 = new Produce(storage,"p3");
Consume c1 = new Consume(storage,"c1");
Consume c2 = new Consume(storage,"c2");
Consume c3 = new Consume(storage,"c3");
p1.start();
p2.start();
p3.start();
c1.start();
c2.start();
c3.start();
}
}
//仓库
class Storage{
//容量
private static final int MAX_SIZE = 10;
//实际存储元素的仓库
private LinkedList<Object> storage = new LinkedList<>();
//生产
public void produce(Object product){
synchronized (storage){
//判断当队列满时,等待消费者消费并通知
while(storage.size() == MAX_SIZE) {
System.out.println(Thread.currentThread().getName()+":队列已满...");
try {
storage.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//插入元素
storage.addLast(product);
System.out.println(Thread.currentThread().getName()+"生产了一个"+product+",队列中还有"+storage.size()+"个元素...");
storage.notifyAll();
}
}
//消费
public void consume(){
synchronized (storage){
//判断队列空时,等待生产者插入元素
while(storage.size() == 0) {
System.out.println(Thread.currentThread().getName()+":队列为空...");
try {
storage.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//取出元素
Object product = storage.remove();
System.out.println(Thread.currentThread().getName()+"消费了一个"+product+",队列中还有"+storage.size()+"个元素...");
storage.notifyAll();
}
}
}
//生产者线程
class Produce extends Thread{
//仓库引用
private Storage storage;
public Produce(Storage storage,String name) {
this.storage = storage;
setName(name);
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
storage.produce(new Random().nextInt(100));
}
}
}
//消费者线程
class Consume extends Thread {
//仓库引用
private Storage storage;
public Consume(Storage storage,String name) {
this.storage = storage;
setName(name);
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
storage.consume();
}
}
}
输出结果
p1生产了一个77,队列中还有1个元素...
p3生产了一个73,队列中还有2个元素...
p2生产了一个38,队列中还有3个元素...
p1生产了一个18,队列中还有4个元素...
p3生产了一个13,队列中还有5个元素...
p2生产了一个44,队列中还有6个元素...
p1生产了一个84,队列中还有7个元素...
p2生产了一个21,队列中还有8个元素...
p3生产了一个18,队列中还有9个元素...
c1消费了一个77,队列中还有8个元素...
c2消费了一个73,队列中还有7个元素...
c3消费了一个38,队列中还有6个元素...
p2生产了一个29,队列中还有7个元素...
p3生产了一个50,队列中还有8个元素...
p1生产了一个24,队列中还有9个元素...
p3生产了一个17,队列中还有10个元素...
p1:队列已满...
p2:队列已满...
p3:队列已满...
c1消费了一个18,队列中还有9个元素...
p3生产了一个93,队列中还有10个元素...
p2:队列已满...
使用Lock和Condition实现
使用Lock和Condition实现生产者-消费者模式与使用Synchronized类似,在上述代码的基础上只需要改动Storage类即可。
//仓库
class Storage{
//仓库容量
private static final int MAX_SIZE = 10;
//实际存储元素的仓库
private LinkedList<Object> storage = new LinkedList<>();
//互斥锁
private final Lock lock = new ReentrantLock();
//仓库满时的条件变量
private final Condition full = lock.newCondition();
//仓库空时的条件变量
private final Condition empty = lock.newCondition();
//生产
public void produce(Object product){
//加锁
lock.lock();
try {
//当队列满时,等待消费者消费并通知
while (storage.size() >= MAX_SIZE) {
try {
System.out.println(Thread.currentThread().getName()+":队列已满...");
full.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//插入元素
storage.add(product);
System.out.println(Thread.currentThread().getName()+"生产了一个"+product+",队列中还有"+storage.size()+"个元素...");
//唤醒消费者线程
empty.signalAll();
}finally {
//解锁
lock.unlock();
}
}
//消费
public void consume(){
//加锁
lock.lock();
try {
//当队列空时,等待生产者插入元素
while (storage.size() == 0) {
System.out.println(Thread.currentThread().getName()+":队列为空...");
try {
empty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//取出元素
Object product = storage.remove();
System.out.println(Thread.currentThread().getName()+"消费了一个"+product+",队列中还有"+storage.size()+"个元素...");
//唤醒生产者线程
full.signalAll();
}finally {
lock.unlock();
}
}
}
使用阻塞队列实现
在介绍阻塞队列时有三种类型的插入和删除元素的方法,其中一直阻塞的put()和take()方法在实现生产者-消费者模式时再适合不过。
class Storage{
//仓库容量
private static final int MAX_SIZE = 10;
//阻塞队列
private LinkedBlockingQueue<Object> storage = new LinkedBlockingQueue<>(MAX_SIZE);
//生产
public void produce(Object product){
//使用put()方法,当队列满时,会一直阻塞
try {
storage.put(product);
System.out.println(Thread.currentThread().getName()+"生产了一个"+product+",队列中还有"+storage.size()+"个元素...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消费
public void consume(){
//使用take()方法,当队列空时,会一直阻塞
try {
Object product = storage.take();
System.out.println(Thread.currentThread().getName()+"消费了一个"+product+",队列中还有"+storage.size()+"个元素...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
信号量实现
管道实现