【JUC 并发编程】— 阻塞队列
Queue
阻塞队列(BlockingQueue)继承自 Queue,先看看 Queue 定义了哪些接口
/**
* Queue 相对于 Collection 中基本的方法,提供了另外的插入,移除和检查方法。
* 每个方法都有两种形式:如果操作失败,一种是直接抛异常,另一种是返回特殊值(null 或者 false)
*/
public interface Queue<E> extends Collection<E> {
/**
* 添加元素,如果队列已满,则抛出 IllegalStateException 异常
*/
boolean add(E e);
/**
* 添加元素
* 添加成功,返回 true,否则 false
* 如果是有界队列,此方法优于 add() 方法,因为 add() 失败了只能抛异常
*/
boolean offer(E e);
/**
* 移除第一个元素
* 如果队列为空,则抛出 NoSuchElementException 异常
*/
E remove();
/**
* 移除第一个元素
* 如果队列为空,返回 null
*/
E poll();
/**
* 返回第一个元素,但不会移除
* 如果队列为空,则抛出 NoSuchElementException 异常
*/
E element();
/**
* 返回第一个元素,但不会移除
* 如果队列为空,则返回 null
*/
E peek();
}
BlockingQueue
JDK 中是这么介绍 BlockingQueue 的
A {@link java.util.Queue} that additionally supports operations
that wait for the queue to become non-empty when retrieving an
element, and wait for space to become available in the queue when
storing an element.
在 Queue 的基础上支持另外两种操作:① 支持阻塞的插入方法:当队列满时,插入线程会被阻塞,直到队列不满。② 支持阻塞的移除方法:队列为空时,获取元素的线程会等待,直到队列为非空。
也就是对应着 put() 和 take() 方法
/**
* 插入元素
* 如果队列满了,则阻塞插入线程
*/
void put(E e) throws InterruptedException;
/**
* 移除元素
* 如果队列为空,则阻塞移除线程,直到队列非空
*/
E take() throws InterruptedException;
Doug Lea 老爷子也是贴心,在 BlockingQueue 源码注释中为我们整理了这些方法
Throws exception | Special value | Blocks | Times out | |
Insert | add(e) | offer(e) | put(e) | offer(e, time, unit) |
Remove | remove() | poll() | take() | poll(time, unit) |
Examine | element() | peek() | not applicable | not applicable |
阻塞队列实现
JDK 7 提供了 7 个阻塞队列实现,如下
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列
- LinkedBlockingQueue:由链表结构组成的有界阻塞队列
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列
- DelayQueue:使用优先级队列实现的无界阻塞队列
- SynchronousQueue:不存储元素的阻塞队列
- LinkedTransferQueue:由链表结构组成的无界阻塞队列
- LinkedBlockingDeque:由链表结构组成的双向阻塞队列
ArrayBlockingQueue
ArrayBlockingQueue 是用数组实现的有界阻塞队列,默认情况下访问队列线程是不公平的,可以在构造方法中指定是否需要保证线程访问的公平性
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
而且可以看到,公平性是使用重入锁来实现的。
LinkedBlockingQueue
LinkedBlockingQueue 是一个用链表实现的有界阻塞队列,默认长度为 Integer.MAX_VALUE
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
PriorityBlockingQueue
PriorityBlockingQueue 是一个支持优先级的无界阻塞队列,默认情况元素采用自然顺序升序排列。
/**
* The comparator, or null if priority queue uses elements'
* natural ordering.
*/
private transient Comparator<? super E> comparator;
public PriorityBlockingQueue(int initialCapacity,
Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
this.comparator = comparator;
this.queue = new Object[initialCapacity];
}
可以自定义类实现 compareTo() 方法来指定元素排序规则,或者初始化 PriorityBlockingQueue 时,指定构造函数 Comparator 来对元素进行排序。
DelayQueue
DelayQueue 是一个支持延时获取元素的无界阻塞队列。队列使用 PriorityQueue 来实现,队列中的元素必须实现 Delayed 接口,在创建元素时可以指定多久才能队列中获取当前元素。只有在延迟期满时才能从队列中获取元素。
SynchronousQueue
SynchronousQueue 是一个不存储元素的阻塞队列。每一个 put 操作必须对应一个 take 操作,否则不能继续添加元素。SynchronousQueue 维护一组线程,这些线程在等待着把元素加入或移除队列。这种实现队列的方式看似很奇怪,但由于可以直接交付工作,从而降低了将数据从生产者移动到消费者的延迟。(在传统的队列中,在一个工作单元可以交付之前,必须通过串行方式首先完成入列[Enqueue]或者出列[Dequeue]等操作)。直接交付方式还会将更多关于任务状态的信息反馈给生产者。当交付被接受时,他就知道消费者已经得到了任务,而不是简单地把任务放入一个队列。仅当有足够多的消费者,并且总是有一个消费者准备好获取交付的工作时,才适合使用同步队列。SynchronousQueue 的吞吐量高于 LinkedBlockingQueue 和 ArrayBlockingQueue。
LinkedTransferQueue
LinkedTransferQueue 是一个由链表结构组成的无界阻塞队列。相对于其他阻塞队列,LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。
transfer 方法
如果当前有消费者正在等待接收元素(消费者使用 take() 方法或者带时间限制的 poll() 方法时),transfer 方法可以把生产者传入的元素立刻 transfer 给消费者。如果没有消费者在等待接收元素,transfer 方法会将元素存放在队列的 tail 节点,并等到该元素被消费者消费了才返回。
tryTransfer 方法
tryTransfer 方法是用来试探生产者传入的元素是否能够直接传给消费者。如果没有消费者等待接收元素,则返回 false。和 transfer 方法的区别是 tryTransfer 方法无论消费者是否接收,方法立即返回,而 transfer 方法是必须等到消费者消费了才返回。
LinkedBlockingDeque
LinkedBlockingDeque 是一个由链表结构组成的双向阻塞队列。所谓双向队列指的是可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。
阻塞队列原理
JDK 使用通知模式来实现生产者线程和消费者线程之间的通信。以 ArrayBlockingQueue 为例
/** 队列元素数组 */
final Object[] items;
/** 队列元素数量 */
int count;
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
等待通知采用的是 Condition 的 await() 方法和 signal() 方法(Condition 源码解析看这里)。
重点看看 put() 方法和 take() 方法
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
insert(e);
} finally {
lock.unlock();
}
}
private void insert(E x) {
items[putIndex] = x;
putIndex = inc(putIndex);
++count;
notEmpty.signal();
}
put() 方法思路:获取锁,如果队列已满,则阻塞生产者线程;如果队列没满,则调用 insert() 方法插入,这时候队列肯定非空,那么就执行 notEmpty.signal() 通知消费者线程。
再来看看 take() 方法
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return extract();
} finally {
lock.unlock();
}
}
private E extract() {
final Object[] items = this.items;
E x = this.<E>cast(items[takeIndex]);
items[takeIndex] = null;
takeIndex = inc(takeIndex);
--count;
notFull.signal();
return x;
}
take() 方法思路:获取锁,如果队列为空,则阻塞消费者线程,否则移除元素。这时候队列没满,则执行 notFull.signal() 通知生产者线程。
参考
- JDK 1.7 源码
- Java并发编程的艺术