引言
在使用线程池 executor 时,有一个参数是阻塞队列 BlockingQueue。什么是阻塞队列呢?阻塞队列就是实现了多线程下生产者-消费者模型的一个东西。生产者将“物品”存入队列,如果队列已满则一直阻塞,直至队列有空闲;消费者从队列中取出“物品”,如果队列为空就一直阻塞,直至队列不为空。对于线程池来说,提交的“任务”就是所谓的物品。
运用阻塞队列的特性,线程池实现了不同的任务提交策略。比如 Executors 中的 newFixThreadPool 和 newSingleThreadExecutor 就是使用了阻塞队列 LinkedBlockingQueue。具体的来说,在executor()方法中,调用了阻塞队列的offer(E e)方法来尝试将任务入队;而工作线程则通过poll(long timeout, TimeUnit unit)或者take()方法来获取要执行的任务。
poll()和take()同样是获取队列中的任务,但是行为却不同:如果队列中没有任务,poll()会直接返回 false,而 take()则会一直阻塞,直至有任务可以被取到。BlockingQueue 中一共定义了四组放入、取出的方法,分别如下:
| 动作 | 抛异常 | 返回值 | 阻塞 | 超时 |
|---|---|---|---|---|
| 放入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
| 取出 | remove() | poll() | take() | poll(time, unit) |
jdk 中的阻塞队列一共 7 个,分别是 ArrayBlockingQueue、LinkedBlockingQueue、LinkedBlockingDeque、PriorityBlockingQueue、DelayQueue、SynchronousQueue、LinkedTransferQueue。
下面就简单分析一下它们。
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(); // condition
notFull = lock.newCondition(); // condition
}
从构造方法里,可以看到 ArrayBlockingQueue 的成员变量有一个可重入锁 lock,以及两个 condition。
lock 主要控制这几个变量的共享:
final Object[] items; // 元素集合
int takeIndex; // 下一个取出元素的下标
int putIndex; // 下一个添加元素的下标
int count; // 元素数量
items 和 count 变量无需解释了,而 takeIndex 和 putIndex 该如何理解呢?这里可以先看一下 put()方法是如何实现的:
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();
}
}
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x; // 设置下标为putIndex的元素
if (++putIndex == items.length) // 从头循环
putIndex = 0;
count++;
notEmpty.signal(); // 唤醒notEmpty
}
由此可见,ArrayBlockingQueue 实际上循环利用了底层的数组。先从头至尾从数组中移除元素,当添加至末尾时,将会从头开始继续添加。而具体的数组有没有满,则是通过 count 和 items.length来判断的。take()方法也是如此,非常类似,这里就不在多述了:
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() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
可以看到,put(e)和take()利用notFull和notEmpty两个 Condition 实现了放入取出的阻塞,那么立即获取返回值的offer(e)和poll()方法是如何实现的呢?实际上非常简单,只要判断count和items.length的大小关系就可以。如果count==items.length,那么队列已满,直接返回 false 就可以:
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length) // 队列已满,直接返回false
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
LinkedBlockingQueue
照样先来看构造方法:
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 时必须传入一个 capacity、也就是容量。所以说 LinkBlockingQueue 是一个无界队列其实是不准确的,它是一个必须指定容量的有界队列,只不过默认的空参数构造方法传入的容量是Integer.MAX_VALUE,所以会看似是无界的。这也是会导致内存溢出的原因。
和 ArrayBlockingQueue 类似,LinkedBlockingQueue 也是通过锁、condition 和count来控制队列的并发和阻塞。但与 ArrayBlockingQueue 不同的是,LinkedBlockingQueue 控制的粒度更加小,不是直接通过一个粗粒度的 ReentrantLock 控制所有贡献变量的访问,而是通过了两把锁和原子类 AtomicInteger:
private final AtomicInteger count = new AtomicInteger(); // 元素计数
private final ReentrantLock takeLock = new ReentrantLock(); // 取出元素的锁
private final Condition notEmpty = takeLock.newCondition(); // 取出元素的condition
private final ReentrantLock putLock = new ReentrantLock(); // 放入元素的锁
private final Condition notFull = putLock.newCondition(); // 放入元素的condition
因为取出和放入使用了不用的锁,所以对于 LinkedBlockingQueue 而言,生产和消费两者完全独立,可以同时进行而不像 ArrayBlockingQueue 一样,会有一端停下来等待。那为什么 ArrayBlockingQueue 不用两把锁呢?因为 ArrayBlockingQueue 底层循环利用了数组,为了保证数组的一致性,必须使用同一把锁。
LinkedBlockingDeque
LinkedBlockingDeque 和 LinkedBlockingQueue 类似,底层都是使用链表结构来存储元素。但不同的是,LinkedBlockingQueue 是一个单向链表,每个结点只存储了 next 引用,而 LinkedBlockingDeque 是双向链表,除了 next 引用外还存储了 prev、也就是上一个结点的引用。对比单向队列 Queue,双端队列 Deque 支持从队列头或者队列尾增加和删除元素,如addFirst(E)、addLast(e)、removeFirst()、removeLast()等方法。
在并发控制上,LinkedBlockingDeque 和 ArrayBlockingQueue 一样,通过全局的一把锁、count 以及 notEmpty、notFull 两个 condition 对象来控制阻塞,这里就不再粘贴代码了。注意到 LinkedBlockingDeque 也是有容量的。
PriorityBlockingQueue
优先堵塞队列是一个无界的阻塞队列。虽然没有最大容量的限制,但是在初始化的时候,可以指定初始容量,在容量不足的时候可以自动扩容。底层是用数组表示的平衡二叉堆,根据自然排序(实现 compareble 接口)或者比较器(comparator 类)排序。同样用一把可重入锁来实现并发,但由于是无界的,所以只有一个 notEmpty 的 condition 对象来控制take()的阻塞,至于put()方法则是永远不会阻塞的。
public void put(E e) {
offer(e); // 直接调用offer()方法,永远不阻塞
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
while ( (result = dequeue()) == null)
notEmpty.await(); // 空队列阻塞
} finally {
lock.unlock();
}
return result;
}
DelayQueue
延时队列 DelayQueue 也是个无界队列,底层是用优先队列 PriorityQueue 实现的。DelayQueue 队列的元素需要实现Delayed接口的getDelay()方法。只有该方法的返回值小于等于 0,才能出列。因为是无界队列,所以put()方法也是直接调用了offer(),并不会产生阻塞:
public void put(E e) {
offer(e);
}
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); // 加锁
try {
q.offer(e); // 进优先队列 PriorityQueue
if (q.peek() == e) { // 添加元素在头部
leader = null; // 因为是新头部,所以等待它的线程设为null
available.signal(); // 通知等待线程
}
return true;
} finally {
lock.unlock(); // 解锁
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek();
if (first == null) // 队列为空则等待
available.await();
else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0) // 延迟时间到,返回元素
return q.poll();
first = null;
if (leader != null) // 有其他的线程等待头结点,则本线程一直等待
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay); // 本线程限时等待头结点
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
SynchronousQueue
SynchronousQueue 是一个非常特殊的队列,它没有容量的概念,当一个线程往里放数据的时候,必须有另一个线程从中取走数据;反之亦然,当一个线程从中取数据的时候,必须有另一个线程往里面放数据。换句话说,就是只有放入与取出操作“配对”,才能成功进行。在线程池中,Executors.newCachedThreadPool()就使用了这个队列。
构造方法如下:
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
从构造方法我们可以知道,SynchronousQueue 内部有个transferer变量。这个变量是对Transferer接口对象的一个引用。通过Transferer对象的不同实现(队列或者堆栈),可以实现公平模式或者非公平模式。同时,这个对象也是实现“配对”操作的关键。
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
if (transferer.transfer(e, false, 0) == null) {
Thread.interrupted();
throw new InterruptedException();
}
}
public E take() throws InterruptedException {
E e = transferer.transfer(null, false, 0);
if (e != null)
return e;
Thread.interrupted();
throw new InterruptedException();
}
可以看到put()和take()主要调用了transferer.transfer(E e,boolean timed,long nanos)方法。当 e 为 null 时,当作消费者处理;当 e 不为 null 时,当作生产者处理。Transferer有两个实现,一个是保证公平性的TransferQueue,另一个是非公平的TransferStack。默认是非公平性的。
这里主要看看TransferQueue的 transfer实现:
E transfer(E e, boolean timed, long nanos) {
QNode s = null;
boolean isData = (e != null); // true:放入操作;false:取出操作
for (;;) {
QNode t = tail;
QNode h = head; // head本身是一个dummy结点,head.next指向真实的第一个结点
if (t == null || h == null)
continue;
if (h == t || t.isData == isData) { // 队列为空,或者操作相同(都是放入或者取出),则入队
QNode tn = t.next;
if (t != tail) // tail被另一个线程改变了
continue;
if (tn != null) { // tail.next被另一个线程改过了,追加到新tail后面
advanceTail(t, tn);
continue;
}
if (timed && nanos <= 0) // 限时等待超时,直接返回null
return null;
if (s == null) // 构建新结点s
s = new QNode(e, isData);
if (!t.casNext(null, s)) // 如果设置tail.next失败,则循环cas,直至成功
continue;
// 执行到这里,说明已经成功设置新的尾部了
advanceTail(t, s); // 设置新的tail
Object x = awaitFulfill(s, e, timed, nanos); // 等待(先自旋后挂起),如果被中断或者限时等待超时则返回s,否则返回配对的对象
if (x == s) { //被取消或者超时
clean(t, s);
return null;
}
// 执行到这里,说明已经成功配对了
if (!s.isOffList()) { // 尚未取消连接
advanceHead(t, s); // unlink if head
if (x != null) // and forget fields
s.item = s;
s.waiter = null;
}
// 配对对象不为null,说明对方是生产者,返回对方生产的数据;配对对象为null,说明对方是消费者,自己是生产者,返回自己生产的数据
return (x != null) ? (E)x : e;
} else { // 操作互补
QNode m = h.next;
if (t != tail || m == null || h != head) // 被另一个线程改变了
continue;
Object x = m.item;
if (isData == (x != null) || // 无法匹配m,说明m已经成功匹配
x == m || // m被取消
!m.casItem(x, e)) { // CAS设置配对的item失败
advanceHead(h, m); // m出队,重试
continue;
}
advanceHead(h, m); // 配对完成,出队
LockSupport.unpark(m.waiter);
return (x != null) ? (E)x : e;
}
}
}
整体来说,是使用一个队列新的任务排队。如果队列为空或者新任务的动作和 tail 结点相同(都是 add/offer/put 或者 remove/poll/take),将会追加到队列尾,并进行等待(先自旋后挂起)。反之,则说明操作互补,那么就会从头开始匹配。这里因为 head 是一个哨兵结点,所以从 head.next 开始尝试配对。如果互补配对成功则出队,反之继续重试。
LinkedTransferQueue
LinkedTransferQueue 是个无界的阻塞队列,实现了 BlockingQueue 和子接口 TransferQueue。TransferQueue 主要描述一个生产者可能会等待消费者取走元素的队列。相对于 BlockingQueue,TransferQueue 增加了这些方法:
// 尝试放入元素,如果有消费者等待则立即放入并返回true,否则返回false
boolean tryTransfer(E e);
// 尝试放入元素,如果没有消费者则等待
void transfer(E e) throws InterruptedException;
// 尝试在一定时间内放入元素
boolean tryTransfer(E e, long timeout, TimeUnit unit)
throws InterruptedException;
// 是否有等待的消费者
boolean hasWaitingConsumer();
// 等待的消费者数量
int getWaitingConsumerCount();
类似于 SynchronousQueue,LinkedTransferQueue 使用了一个xfer(E e, boolean haveData, int how, long nanos)方法实现 put、take 和 transfer 的相关方法:
public void put(E e) {
xfer(e, true, ASYNC, 0);
}
public E take() throws InterruptedException {
E e = xfer(null, false, SYNC, 0);
if (e != null)
return e;
Thread.interrupted();
throw new InterruptedException();
}
public boolean tryTransfer(E e) {
return xfer(e, true, NOW, 0) == null;
}
下面就来分析一下这个xfer:
private E xfer(E e, boolean haveData, int how, long nanos) {
if (haveData && (e == null))
throw new NullPointerException();
Node s = null; // the node to append, if needed
retry:
for (;;) { // restart on append race
for (Node h = head, p = h; p != null;) { // find & match first node
boolean isData = p.isData; // true是生产者,false是消费者
Object item = p.item;
// item==p说明被取消,item==null&&isData==false说明是个消费结点,item!=null&&isData==true说明是个生产结点,这两种结点都没有匹配成功
if (item != p && (item != null) == isData) { // unmatched
if (isData == haveData) // can't match
break;
if (p.casItem(item, e)) { // match
for (Node q = p; q != h;) {
Node n = q.next; // update by 2 unless singleton
if (head == h && casHead(h, n == null ? q : n)) {
h.forgetNext();
break;
} // advance and retry
if ((h = head) == null ||
(q = h.next) == null || !q.isMatched())
break; // unless slack < 2
}
LockSupport.unpark(p.waiter);
return LinkedTransferQueue.<E>cast(item);
}
}
Node n = p.next;
p = (p != n) ? n : (h = head); // Use head if p offlist
}
if (how != NOW) { // No matches available
if (s == null)
s = new Node(e, haveData);
Node pred = tryAppend(s, haveData);
if (pred == null)
continue retry; // lost race vs opposite mode
if (how != ASYNC) // add,offer,put时值为ASYNC,所以生产时不堵塞
return awaitMatch(s, pred, e, (how == TIMED), nanos);
}
return e; // not waiting
}
}
方法主要通过haveData字段判断是生产者还是消费者。每当放入或者取出结点 e 的时候,遍历队列,如果发现遍历的结点 e1 和 e 互补配对(一个生产者,一个消费者),那么就返回匹配的结点;如果没找到匹配的结点,那么就把该结点入队。