阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。
put()
方法。take()
方法。BlockingQueue 常用于生产者-消费者场景,生产者是往队列里添加元素的线程,消费者是从队列里取元素的线程。BlockingQueue就是存放元素的容器。
阻塞队列提供了四组不同的方法用于插入、移除、检查元素:
方法/处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
检查方法 | element() | peek() | - | - |
IllegalStateException(“Queue full”)
异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException
异常 。以下是 7 种阻塞队列:
FIFO 队列 :LinkedBlockingQueue、ArrayBlockingQueue(固定长度)
优先级队列 :PriorityBlockingQueue
ArrayBlockingQueue 是一个用数组实现的有界阻塞队列。按照先进先出(FIFO)的原则对元素进行排序。
可以初始化队列大小, 且一旦初始化,容量不能改变。构造方法中的 fair 表示控制对象的内部锁是否采用公平锁,默认是非公平锁。并发控制以及访问者的公平性是使用可重入锁 ReentrantLock 实现的。
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();
}
ArrayBlockingQueue
默认情况下不保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 ArrayBlockingQueue
。而非公平性则是指访问的顺序不是遵守严格的时间顺序,有可能导致饥饿现象。如果保证公平性,通常会降低吞吐量。
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
ArrayBlockingQueue
实现并发同步的原理就是,读操作和写操作都需要获取到 AQS 独占锁才能进行操作。如果队列为空,这个时候读操作的线程进入到读线程队列排队,等待写线程写入新的元素,然后唤醒读线程队列的第一个等待线程。如果队列已满,这个时候写操作的线程进入到写线程队列排队,等待读线程将队列元素移除腾出空间,然后唤醒写线程队列的第一个等待线程。
LinkedBlockingQueue 底层是基于单链表实现的阻塞队列,可以是无界队列也可以是有界队列。可以通过构造方法指定容量大小来创建有界队列,也可以不指定容量大小,默认队列的大小是Integer.MAX_VALUE
,此时创建的就是无界队列。我们可以通过查看源码来验证:
static class Node<E> {
E item;
// 只有后驱指针,说明是单链表
Node<E> next;
Node(E x) { item = x; }
}
// 容量限制,如果没有设置,则为 Integer.MAX_VALUE
private final int capacity;
/**
* 无界队列
*/
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
/**
* 有界队列
*/
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
LinkedBlockingQueue
实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
// 由 take、poll 等持有的锁
private final ReentrantLock takeLock = new ReentrantLock();
// 由 put、offer 等持有的锁
private final ReentrantLock putLock = new ReentrantLock();
图源:https://javadoop.com/post/java-concurrent-queue
PriorityBlockingQueue
是一个支持优先级的无界阻塞队列,底层是基于数组的二叉堆实现的,默认情况下元素采用自然顺序升序排序。也可以通过自定义类实现 compareTo()
方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator
来指定排序规则。并发控制采用的是可重入锁 ReentrantLock
。
PriorityBlockingQueue
是无界队列,初始化时指定的队列大小。无界体现在后面插入元素的时候,如果空间不够的话会自动扩容。
简单地说,它就是
PriorityQueue
的线程安全版本。不可以插入 null 值。同时,插入队列的对象必须是可比较大小的(comparable),否则报ClassCastException
异常。它的插入操作 put 方法不会阻塞,因为它是无界队列,take 方法在队列为空的时候会阻塞。
如果队列为空,消费者会阻塞一直等待,当生产者添加元素时,便需要通知消费者当前队列有元素;而队列满时,生产者需要阻塞,消费者消费了队列一个元素后,又需要通知生产者当前队列可用。所以阻塞队列需要让生产者与消费者进行通信,核心是:利用 Condition 实现等待 / 通知机制。
下列我们通过 ArrayBlockingQueue
源码来探究如果利用 Condition 实现等待 / 通知机制:
//数据元素数组
final Object[] items;
//内部锁
final ReentrantLock lock;
//消费者监视器
private final Condition notEmpty;
//生产者监视器
private final Condition notFull;
public ArrayBlockingQueue(int capacity, boolean fair) {
// ......
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
执行 put
的线程首先需要竞争 lock 锁,没有获取到锁则自旋竞争锁。当往队列里插入一个元素时,如果队列不可用,会调用 Condition
的 await
方法阻塞当前线程,直到被消费者唤醒。可以看到 await
方法阻塞生产者主要通过 LockSupport.park(this)
来实现,而 park
方法又是通过调用 unsafe.park
方法来阻塞当前线程,这是一个 native 方法,需要等到 unpark
执行或者线程被中断该方法才会返回。
// ArrayBlockingQueue.put(E e)
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 自旋获取锁
try {
while (count == items.length)
notFull.await(); // 阻塞生产者
enqueue(e);
} finally {
lock.unlock();
}
}
// ArrayBlockingQueue.enqueue(E e)
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal(); // 唤醒一个消费者
}
// ConditionObject.await()
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
long savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this); // 阻塞当前线程
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// ......
}
// LockSupport.park()
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L); // 阻塞当前线程
setBlocker(t, null);
}
消费者从队列中获取元素时,若队列为空,会阻塞当前线程;而成功获取元素后,又会唤醒生产者(如果生产者因为队列满而阻塞的话)。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 自旋获取锁
try {
while (count == 0)
notEmpty.await(); // 阻塞消费者
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
final Object[] items = this.items;
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal(); // 唤醒一个生产者
return x;
}
参考资料
《Java 并发编程的艺术》