前言
通常来说,Queue队列的特性一般为FIFO(先进先出),在队列的特性上在增加一个阻塞的特性,这就是BlockingQueue,这很像一个生产者-消费者模式,生产者负责将某些元素放入队列,消费者负责从队列中取元素,阻塞-通知的特性让这种模式更加高效,例如有元素我就会唤醒阻塞的线程,而不是一直去主动轮询是否有任务,在线程池中也利用了阻塞队列的特性,实现了线程任务的提交和获取执行,所以配置一个合适的线程池之前,你需要了解阻塞队列的实现和使用。
本篇文章的议题围绕BlockingQueue几个常用的阻塞队列实现类:
- SynchronousQueue
- ArrayBlockingQueue
- LinkedBlockingQueue
但阻塞队列中有些方法是会一直阻塞,有些方法又不会,每个方法都有其特有的场景和作用,在这里会介绍其源码,分析其原理,让读者更好的使用和了解阻塞队列的特性。
不同阻塞队列实现类,内部的数据结构和行为都存在一定的不同,所以本篇文章的维度将在不同阻塞队列的实现和实现对应的一系列存取方法来展开。
阻塞队列API
那么阻塞队列都有哪些方法可供用户使用呢?直接来看J.U.C中的BlockingQueue这个接口
存放元素
boolean add(E e)
Inserts the specified element into this queue if it is possible to do so immediately without violating capacity restrictions, returning {@code true} upon success and throwing an {@code IllegalStateException} if no space is currently available.
When using a capacity-restricted queue, it is generally preferable to use {@link #offer(Object) offer}.
此方法上的注释说明了此方法的特性:
- 会立即返回
- 返回true代表添加成功
- 抛出IllegalStateException异常代表容量限制
- 如果是一个有容量限制的队列,一般来说更偏向使用offer方法去存放元素
boolean offer(E e)
Inserts the specified element into this queue if it is possible to do so immediately without violating capacity restrictions, returning {@code true} upon success and {@code false} if no space is currently available. When using a capacity-restricted queue, this method is generally preferable to {@link #add}, which can fail to insert an element only by throwing an exception.
- 立即返回
- 返回ture代表添加成功
- 返回false代表添加失败,当前没有可用空间
- 在有容量限制的队列中,通常这个方法比add要好
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
Inserts the specified element into this queue, waiting up to the specified wait time if necessary for space to become available.
- 会阻塞一段用户自定的timeout时间,为阻塞一段时间版的offer
- 因为会阻塞,所以有可能抛出InterruptedException中断的异常
void put(E e) throws InterruptedException
Inserts the specified element into this queue, waiting if necessary for space to become available.
- 如果队列没有空间存放元素会一直阻塞
- 若方法返回,代表添加成功
- 因为会阻塞,所以此方法有可能抛出InterruptedException中断的异常
获取元素
E take() throws InterruptedException
Retrieves and removes the head of this queue, waiting if necessary until an element becomes available.
- 取出然后删除队列的头元素
- 如果没有元素在队列,方法会一直阻塞
- 同样需要处理中断异常
E poll(long timeout, TimeUnit unit) throws InterruptedException
Retrieves and removes the head of this queue, waiting up to the specified wait time if necessary for an element to become available.
- 取出然后删除队列头元素,与上面方法的不同点在于此方法指定了阻塞的时间
小结
这里对阻塞队列的API做一个小结
存数据操作 | 是否会阻塞 | 是否可以指定阻塞时间 |
---|---|---|
add(E e) | 不会 | 不可以 |
offer(E e) | 不会 | 不可以 |
offer(E e, long timeout…) | 会 | 可以 |
put(E e) | 会 | 不可以 |
例如,我们需要只阻塞一段时间,那就需要使用带时限的offer方法,若想要阻塞直到可以放入元素,那就需要put或者带时限的offer,如果不想阻塞,只想尝试put一下,可以使用offer方法,add方法不推荐使用。
取数据操作 | 是否会阻塞 | 是否可以指定阻塞时间 |
---|---|---|
take() | 会 | 不可以 |
poll(long timeout…) | 会 | 可以 |
取元素操作相对比较简单一些,若不想要阻塞,只是尝试获取,可以使用poll(0)
ArrayBlockingQueue
这个阻塞队列是一个有界队列,其界限特性在其构造函数中就可以看出
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
// 内部的数据结构,数组
this.items = new Object[capacity];
// 阻塞的特性的实现,类似获取特定锁下的wait-notify操作
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
由此可见,在创建一个ArrayBlockingQueue时必须指定一个容量,队列内部的数据结构则为一个数组来存放
/** The queued items */
final Object[] items;
很简单,在队列初始化之后会分配一段连续的内存(数组),以数组作为其数据结构。下面来看看开头我们提到的API都是怎么实现的
其中add方法为父类AbstractQueue的模版方法
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
其实就只是offer而已,只不过add会抛出异常,建议使用offer方法,省去处理异常这一步
offer(无时限)
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
// 因为要使用condition(wait-notify)特性,所以需要获取锁
// 重复逻辑下面都会出现,不再赘述
lock.lock();
try {
// 当前容量等于数组长度,代表队列满了,直接返回false
if (count == items.length)
return false;
else {
// 元素入队
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
很简单,由于offer(E e)方法不具有阻塞特性,所以在队列满的时候直接返回false
offer(有时限)
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
// 值得一提,这里可响应中断
lock.lockInterruptibly();
try {
// 如果队列满了
while (count == items.length) {
if (nanos <= 0)
return false;
// 则等待一个限定的时间
nanos = notFull.awaitNanos(nanos);
}
// 入队
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
没什么好说的,时限等待是通过Condition的实现来做的。这里enqueue是通用入队方法,在后面再详细分析
put(E e)
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 如果队列满了
while (count == items.length)
// 类似wait方法,等待有线程向队列添加元素,就会唤醒
notFull.await();
// 入队
enqueue(e);