在设计任务处理系统的时候,很自然地想到使用生产/消费者模式。任务由生产者生产完成,然后交由消费者(通常是业务相关的处理器)进行消费,完成任务的处理。由于生产者和消费者的处理能力不可能完全一致,参考实际生活中生产线或工厂库存,可使用Queue来对二者进行隔离。生产者将任务生产完毕之后,不是直接交由消费者来进行立即消费,而是将其加入到Queue中;消费者从Queue中获取任务,然后进行任务分配处理。通过Queue进行隔离之后,生产者和消费者的数目可以不同,通常而言,消费者会是任务处理中的瓶颈,因此这种方式更适宜少生产者,多消费者的业务场景。
常用的BlockingQueue有ArrayBlockingQueue和LinkedBlockingQueue两种。
有关BlockingQueue接口中所定义的方法可参考其源码,为方便归纳,我们按照是否会阻塞、当队列满或空时是否抛异常,将入队/出队方法分成以下三类。
add/remove | offer/poll | put/take | |
队列满或队列空时是否抛出异常 | 是 | 否 | 否 |
是否阻塞添加/获取 | 否 | 否 | 是 |
下边我们对这两种BlockingQueue展开详细的介绍。
其特点是存储介质为数组,即Queue中的元素存储在数组中。
add/remove方法对于队列满或空时的处理方式一样--抛异常。这种调用方式,通常而言并不优雅,因此使用场景也比较少见。
先来看add方法:其调用父类AbstractQueue的add方法,并在其内部调用offer方法。当队列没有满的时候通过offer方法正常加入元素,如果队列已满抛异常。
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
接下来看remove方法:其实际是调用的父类AbstractQueue中的remove方法。当队列为空时,poll方法返回的x为null,因此抛异常;如果不为空,则正常返回poll方法获取的队列元素x。
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
当队列已满或为空时,调用offer/poll方法为非阻塞的,通过返回值来告知调用方是否完成入队操作(true or false)或是否获取到队列中的元素(null or 非null)。这种方式相对而言比较优雅,所以在实际场景中比较常见。
首先来看一下offer方法:首先获取锁,注意获取到的是ReentrantLock,而不是synchronized,原因是ReentrantLock是可重入的,并且可设置锁超时,可避免synchronized引发的死锁等问题。接下来检查是否队列已满,如果已满,返回false,表示本次入队操作失败;否则进行入队操作。最后解锁。
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
其中的入队方法--enqueue的操作逻辑如下所示。将传入的元素置入游标--putIndex位置,将putIndex自增1。如果游标已达到最后一个元素的位置,则变更到数组的0位置。每次入队操作完成之后,调用notEmpty Condition的通知方法(这是一个注册在锁上的Condition),通知其他线程队列已有数据插入,可以进行消费。
/**
* Inserts element at current put position, advances, and signals.
* Call only when holding lock.
*/
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
接下来看一下poll方法,其执行逻辑与offer相反。首先获取锁,然后检验当前队列中的元素个数。如果为0,表示队列为空,返回null;否则执行出队操作。
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
出队操作方法执行逻辑如下。出队操作方法中,使用了takeIndex作为队列数据消费的游标,初始值为0。每次完成出队消费时,将takeIndex位置的元素变为null,然后将游标自增1。当游标达到队列尾部之后,设置为0。最后通知其他线程,队列不满了(notFull.signal)。
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和take。这种在实际使用中也比较常见,主要用于生产者-消费者模式的实现。
首先看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();
}
}
put方法在实现过程中,与offer方法不同的一点在于,若队列已满,则阻塞;直到队列不满时,执行入队操作。
take是与put方法相对应的阻塞地获取队列头部元素的方法。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
顾名思义,其特点是存储介质为链表数据结构。
其add/remove方法是使用的父类AbstractQueue的方法,在此不再赘述。
offer与poll方法的执行逻辑,与ArrayBlockingQueue比较类似。不同的地方在于,LinkedBlockingQueue在底层是使用链表进行存储的,链表节点抽象为如下结构:
static class Node {
E item;
/**
* One of:
* - the real successor Node
* - this Node, meaning the successor is head.next
* - null, meaning there is no successor (this is the last node)
*/
Node next;
Node(E x) { item = x; }
}
即保存有元素item及后继节点next,也就是说,LinkedBlockingQueue的存储介质是单向链表(因为仅保留有后继节点的引用)。
offer方法执行逻辑如下:
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity)
return false;
int c = -1;
Node node = new Node(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return c >= 0;
}
首先检测是否队列已满,已满的话直接返回false;否则将元素build成链表节点,获取入队锁,将链表节点入队,递增队列元素个数。如果递增后的元素个数+1后仍小于队列容量,通知其他线程队列非空。完成以上操作之后,解锁,对于加入第一个元素的情况,通知等待take锁的线程队列非空。
接下来关注poll方法,其实现逻辑与offer的实现逻辑相反。
当队列中为空时,返回null;否则尝试获取take锁,执行出队操作(将头结点摘除,将第二结点作为新的头结点)。出队后,只要队列中仍有元素,则通知等待take锁的线程队列非空。执行完毕之后,解锁。
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0)
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
if (count.get() > 0) {
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
最后我们来看一下阻塞版的入队和出队方法。
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node node = new Node(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
其中体现阻塞的语句是:
notFull.await。
相类似的,take方法的实现如下所示。其中体现阻塞的语句是:
notEmpty.await。
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}