阻塞队列与其他类型的队列不同的地方在于阻塞,即对于生产者和消费者两端来说,有任何一端的速度过快时,阻塞队列可以把过快的速度降下来。例如对于一个大小为10的阻塞队列,当生产者线程过快时,在某个时刻队列就会被装满,此时生产者线程被阻塞直到队列中有空的位置;当消费者线程过快时,在某个时刻队列是空的,此时消费者线程被阻塞直到队列中有元素。
Java中的阻塞队列的定义是BlockingQueue,它继承了队列Queue接口,我们先来了解下Queue接口。
Queue是队列的顶级接口,定义了一些出队和入队的操作,这些操作以及它们的作用如下:
BlockingQueue继承了Queue接口,在其基础上添加了几个用于支持阻塞特性的方法,其中最关键的是take()方法和put()方法。
Queue接口和BlockingQueue接口的方法都是出队、入队或访问队首元素的方法,总结如下表:
作用\效果 |
不满足时抛异常 |
不满足时返回特定值 |
阻塞 |
阻塞指定时间 |
入队 |
add(e) |
offer(e) |
put(e) |
offer(e,time,unit) |
获取队首元素并出队 |
remove() |
poll() |
take() |
poll(time,unit) |
获取队首元素 |
element() |
peek() |
不支持 |
不支持 |
阻塞队列根据其容量的大小,可以分为有界和无界两种。其中无界队列并不是真正的无界,只是表示可以容纳非常多的元素,例如LinkedBlockingQueue阻塞队列的上限是Integer.MAX_VALUE;有界队列的容量是有限的,例如ArrayBlockingQueue是由数组实现的,如果容量满了也不会扩容。
BlockingQueue是线程安全的,因此即便生产者和消费者都是多线程的,使用阻塞队列时也不会发生线程安全问题。
队列还能起到隔离的作用,将具体任务与执行任务解耦,即将任务放到阻塞队列中,放任务的线程与执行任务的线程是不相关的,提高了安全性。
BlockingQueue接口的实现类都在JUC包中,它们的区别主要体现在存储结构和元素操作的不同。常见的阻塞队列如下:
ArrayBlockingQueue是最典型的有界阻塞队列,其内部是用数组来存储元素的,因此初始化时需要指定容量大小。在生产者-消费者模型中,如果生产速度和消费速度基本匹配,可以优先考虑使用ArrayBlockingQueue。
ArrayBlockingQueue是基于数组实现的,因此内部有一个数组类型的属性items以及表示数组实际存储元素的数量count。
final Object[] items;
int count;
ArrayBlockingQueue遵循队列“先进先出”的原则,在队尾将元素入队,在队首将元素出队,因此还需要两个“指针”来分别表示出队的数组下标和入队的数组下标。
int takeIndex;//出队指针
int putIndex;//入队指针
ArrayBlockingQueue实现线程安全的方式是使用一个ReentrantLock独占锁,由于队列为空或队列满的时候需要分别阻塞消费者线程和生产者线程,因此还需要两个条件队列。
final ReentrantLock lock;
private final Condition notEmpty;//当队列为空时,阻塞消费者线程
private final Condition notFull;//当队列已满时,阻塞生产者线程
ArrayBlockingQueue由于是基于数组实现的,因此至少要传入数组的容量大小。
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
另外,还可以借用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还提供了一个可以直接将已有元素的集合转成阻塞队列的构造函数。
public ArrayBlockingQueue(int capacity, boolean fair,
Collection extends E> c) {
this(capacity, fair);
final ReentrantLock lock = this.lock;
lock.lock(); // Lock only for visibility, not mutual exclusion
try {
int i = 0;
try {
for (E e : c) {
checkNotNull(e);
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
ArrayBlockingQueue阻塞队列核心的方法就是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();
}
}
enqueue()入队方法的实现如下。
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
take()方法的作用是返回并移除队首的元素,如果队列为空,则阻塞当前消费者线程,实现如下:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
dequeue()出队方法的实现如下。
private E dequeue() {
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;
}
ArrayBlockingQueue是一个基于静态数组实现的有界阻塞队列,遵循“先进先出”的原则。静态数组表示队列长度是固定的,没有扩容机制。另外在静态数组中没有元素的位置存储的是null,这一点从上面的源码也可以看出来。
ArrayBlockingQueue使用了一个ReentrantLock锁,这意味着其存取操作加的都是同一把锁,即其存取操作不能同时进行,存储操作排斥。
ArrayBlockingQueue在入队时从队尾(下标putIndex处)添加元素,如果当前队列已满则调用notFull.await()将当前生产者线程阻塞,否则将putIndex的值加1,如果此时putIndex的值等于数组大小,则将其重置为0,最后由于队列中已有至少一个元素,调用notEmpty.signal()将可能阻塞的消费者线程转移到同步等待队列;在出队时从队首(下标takeIndex处)取出元素,如果当前队列为空则调用notEmpty.signal()将当前消费者线程阻塞,否则将takeIndex的值加1,如果此时takeIndex的值等于数组大小,则将其重置为0,最后由于队列中至少有一个空闲空间,调用notFull.signal()将可能阻塞的生产者线程转移到同步等待队列。
LinkedBlockingQueue是基于单链表实现的无界阻塞队列,也可以通过传入参数来指定队列大小。
由于可以通过传入参数来指定LinkedBlockingQueue队列的大小,因此需要有属性来存储其大小,另外同样需要存储实际的元素数量。
private final int capacity;//队列大小
private final AtomicInteger count = new AtomicInteger();//实际存储的元素数量
LinkedBlockingQueue是基于单链表实现的,保存单链表的首节点和尾节点也是必要的。
transient Node head;//链表首节点
private transient Node last;//链表尾节点
ArrayBlockingQueue只使用了一个ReentrantLock锁,因此其存取操作是互斥的,这在性能上多少会有一些影响。而LinkedBlockingQueue在此处做了改进,为存操作和取操作分别使用了一个ReentrantLock锁。
private final ReentrantLock takeLock = new ReentrantLock();//取锁
private final ReentrantLock putLock = new ReentrantLock();//存锁
由于条件变量Condition是与ReentrantLock锁关联的(必须在与其对应的锁内才能执行相关阻塞和唤醒方法),因此用于阻塞消费者线程的notEmpty条件变量需要由取锁来获取,用于阻塞生产者线程的notFull条件变量需要由存锁来获取。
private final Condition notEmpty = takeLock.newCondition();//取锁获取用于阻塞消费者线程的条件变量
private final Condition notFull = putLock.newCondition();//存锁获取用于阻塞生产者线程的条件变量
LinkedBlockingQueue默认情况下的容量是int的最大值,其无参构造函数实现如下。
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
也可以通过传入一个小于int最大值的参数来指定队列大小,这时LinkedBlockingQueue就是一个有界阻塞队列,除此外,在构造函数中将单链表的头尾节点初始化,都指向一个节点。
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node(null);
}
LinkedBlockingQueue也可以通过传入一个有值的集合来转换为阻塞队列。
public LinkedBlockingQueue(Collection extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // Never contended, but necessary for visibility
try {
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
put()方法在LinkedBlockingQueue队列的实现如下。
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node node = new Node(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
enqueue()入队方法的实现如下,单链表的入队比较简单,只需要将队尾的next指针和last指针先后指向当前node节点即可。
private void enqueue(Node node) {
last = last.next = node;
}
take()方法的实现如下。
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;
}
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
dequeue()出队方法的实现如下,需要注意的是head指针指向的是构造函数初始化时的node节点,第一次执行出队时移除的也是该node节点,但返回的是head的next指针指向的第一个节点。最后将head指针指向队列的第一个节点,并将其节点的值item置为null。
private E dequeue() {
Node h = head;
Node first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
LinkedBlockingQueue是基于单向链表实现的无界阻塞队列,遵循“先进先出”的原则。可以在构造函数中传入指定容量来使其变为有界阻塞队列,并且无界也是有最大值的,最大值为Integer.MAX_VALUE。
LinkedBlockingQueue为存操作和取操作分别都分配了一把ReentrantLock锁,因此其存取操作可以并发执行,效率比存取互斥的ArrayBlockingQueue高。这也是线程池中使用LinkedBlockingQueue而不使用ArrayBlockingQueue的原因。LinkedBlockingQueue还有一点优于ArrayBlockingQueue的地方在于:ArrayBlockQueue在每次put()和take()后都会调用notEmpty或者notFull的signal()方法来唤醒可能存在阻塞的消费者线程或者生产者线程,而ArrayBlockingQueue只会在队列为空时唤醒消费者线程、队列刚好不满时唤醒生产者线程,避免了不必要的线程调度。
LinkedBlockingQueue在队尾的last指针处入队,在队首的head指针处出队。
LinkedBlockingQueue在执行删除方法remove()时存锁和取锁都会加锁解锁。
LinkedBlockingDeque是基于双向链表实现的无界阻塞队列,可以指定容量变为有界阻塞队列。LinkedBlockingDeque与LinkedBlockingQueue很相似,主要有以下不同:
SynchronousQueue是基于链表实现的没有数据缓存的阻塞队列,其队列容量为0,它只是多个线程之间数据交换的媒介。SynchronousQueue最大的特点是生产者线程和消费者线程需要同步地存取数据,当只有生产者线程访问阻塞队列时会被阻塞,直到消费者线程访问阻塞队列获取到该生产者线程的数据;当只有消费者线程访问阻塞队列时也会被阻塞,直到生产者线程访问阻塞队列提供数据给该消费者线程。
SynchronousQueue有公平模式和非公平模式的实现。公平模式使用的是队列数据结构,遵循“先进先出”的原则;非公平模式使用的是栈
数据结构,遵循“先进后出”的原则。
Transferer是SynchronousQueue中的一个抽象内部类,定义了抽象方法transfer(),SynchronousQueue的存取操作都调用了该方法。SynchronousQueue的公平模式和非公平模式分别定义了一个类且继承了Transferer类,其中公平模式的类是TransferQueue,非公平模式的类是TransferStack。
SynchronousQueue分为公平模式和非公平模式,默认情况是非公平模式。
public SynchronousQueue() {
this(false);
}
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue() : new TransferStack();
}
SynchronousQueue的put()方法和take()方法的主要逻辑都在transfer()方法中,只是在调用transfer()时put()方法需要传递生产者数据,take()没有传递数据而已。
put()方法实现如下:
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();
}
}
take()方法实现如下:
public E take() throws InterruptedException {
E e = transferer.transfer(null, false, 0);
if (e != null)
return e;
Thread.interrupted();
throw new InterruptedException();
}
公平模式的Transferer的实现是TransferQueue类,使用的是队列数据结构,为了维护此数据结构在其内部定义了QNode链表节点类。QNode的作用是存储生产者线程或消费者线程信息,其主要属性包含以下几种:
TransferQueue的构造函数实现如下,其中head为单链表的头节点,tail为尾节点。
TransferQueue() {
QNode h = new QNode(null, false); // initialize to dummy node.
head = h;
tail = h;
}
由于SynchronousQueue的put()和take()调用的都是transfer()方法,因此我们主要介绍transfer()。公平模式的transfer()方法的实现如下:
E transfer(E e, boolean timed, long nanos) {
QNode s = null; /
boolean isData = (e != null);
for (;;) {
QNode t = tail;
QNode h = head;
if (t == null || h == null)
continue;
if (h == t || t.isData == isData) {
QNode tn = t.next;
if (t != tail)
continue;
if (tn != null) {
advanceTail(t, tn);
continue;
}
if (timed && nanos <= 0)
return null;
if (s == null)
s = new QNode(e, isData);
if (!t.casNext(null, s))
continue;
advanceTail(t, s);
Object x = awaitFulfill(s, e, timed, nanos);
if (x == s) {
clean(t, s);
return null;
}
if (!s.isOffList()) {
advanceHead(t, s);
if (x != null)
s.item = s;
s.waiter = 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) ||
x == m ||
!m.casItem(x, e)) {
advanceHead(h, m);
continue;
}
advanceHead(h, m);
LockSupport.unpark(m.waiter);
return (x != null) ? (E)x : e;
}
}
}
Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Thread w = Thread.currentThread();
int spins = ((head.next == s) ?
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
if (w.isInterrupted())
s.tryCancel(e);
Object x = s.item;
if (x != e)
return x;
if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
s.tryCancel(e);
continue;
}
}
if (spins > 0)
--spins;
else if (s.waiter == null)
s.waiter = w;
else if (!timed)
LockSupport.park(this);
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}
当前SynchronousQueue阻塞队列首次被一个生产者线程或一个消费者线程访问时会经历以下步骤:
此时再使用一个与上面的模式相同的线程(生产者线程或消费者线程)再次调用transfer()方法,所经历的步骤与上面一样。如果这两次调用都是生产者线程调用put(),则目前就有两个生产者线程在阻塞,否则有两个消费者线程在阻塞。
随后再使用一个与前两次调用模式不同的线程调用transfer()方法,步骤1与上面相同,从步骤2开始有区别,具体步骤如下:
PriorityBlockingQueue是一个基于数组+二叉堆实现的支持按照优先级排序的无界阻塞队列,优先级高的先出队,优先级低的后出队。其中优先级取决于在初始化PriorityBlockingQueue时是否传入比较器,如果传入了比较器则按照比较器给定的顺序排序,否则就按照自然顺序排序。
PriorityBlockingQueue只使用了一把ReentrantLock锁,即它的存取操作是互斥的。
PriorityBlockingQueue在入队时不阻塞,这一点与前面介绍的阻塞队列稍有不同;出队时直接将堆顶的元素出队即可,因此堆顶的元素的优先级是最高的(例如最小堆的堆顶的元素的值是最小的,最大堆的堆顶的元素的值是最大的)。
DelayQueue是基于优先级队列PriorityQueue实现的无界阻塞队列,对于DelayQueue来说延时越低优先级越高,因此先出队的是延时低的元素。
通常可以从以下五个角度来选择合适的阻塞队列: