前言
ArrayBlockingQueue
是基于数组实现的阻塞队列,本文将对ArrayBlockingQueue
的put()
和take()
方法的阻塞逻辑进行分析。
正文
ArrayBlockingQueue
的类图如下所示。
ArrayBlockingQueue
中持有一把重入锁lock,和一对Condition
即notEmpty和notFull,这些都是在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();
notFull = lock.newCondition();
}
下面先分析ArrayBlockingQueue
的put()
方法,如下所示。
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
// 加锁并响应中断
lock.lockInterruptibly();
try {
// 如果当前阻塞队列已满,则在notFull上进行等待
while (count == items.length)
notFull.await();
// 如果当前阻塞队列未满,则将本次添加的元素入队列
enqueue(e);
} finally {
lock.unlock();
}
}
ArrayBlockingQueue
的put()
方法会先加锁,然后判断当前阻塞队列是否已满,如果已满则在notFull上进行等待(等待当前阻塞队列未满),否则调用enqueue()
方法将本次添加的元素入队列。分析enqueue()
方法前,先再看一下ArrayBlockingQueue
的take()
方法的实现,如下所示。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 加锁并响应中断
lock.lockInterruptibly();
try {
// 如果当前阻塞队列为空,则在notEmpty上进行等待
while (count == 0)
notEmpty.await();
// 如果当前阻塞队列不为空,则将队列头元素出队列并返回
return dequeue();
} finally {
lock.unlock();
}
}
ArrayBlockingQueue
的take()
方法也会先加锁,然后判断当前阻塞队列是否为空,为空则在notEmpty上进行等待(等待当前阻塞队列不为空),否则调用dequeue()
方法将队列头元素出队列并返回。
现在已知,当ArrayBlockingQueue
已满时,调用put()
方法的线程会在notFull上进行等待,当ArrayBlockingQueue
为空时,调用take()
方法的线程会在notEmpty上进行等待。那么现在假如ArrayBlockingQueue
已满,线程中调用了put()
方法,此时线程阻塞在notFull上,相应的唤醒等待的操作就一定会出现在有元素出队列的时候,在take()
方法的最后调用了dequeue()
方法来将队列头元素出队列,下面看一下其实现。
private E dequeue() {
final Object[] items = this.items;
// 将items数组中takeIndex位置的元素获取出来
// takeIndex位置的元素就是队列头元素
E x = (E) items[takeIndex];
// 置items数组中takeIndex位置为空
items[takeIndex] = null;
if (++takeIndex == items.length)
// 本次队列头元素在items数组最后一个位置
// 则下一次队列头元素在items数组第一个位置
takeIndex = 0;
// 本次队列头元素出队列后,阻塞队列元素个数减1
count--;
// 因为发生了队列元素出队列,所以迭代器也要更新
if (itrs != null)
itrs.elementDequeued();
// 因为发生了队列元素出队列,此时阻塞队列一定不满
// 那么将在notFull上第一个等待的线程唤醒
notFull.signal();
return x;
}
首先dequeue()
方法中有一个叫做takeIndex的整型变量,该变量默认情况下初始值为0,作为元素数组items的下标索引使用,每次队列元素出队列时,均会让items数组中takeIndex索引位置的元素出队列,所以可以理解为takeIndex永远指向队列头。其次调用dequeue()
的线程一定会持有锁,并且队列头元素出队列后阻塞队列也一定不满,所以dequeue()
方法最后还会调用notFull的signal()
方法,唤醒第一个因为调用put()
方法时队列已满而在notFull上等待的线程。
现在假如ArrayBlockingQueue
为空,线程中调用了take()
方法,此时线程阻塞在notEmpty上,相应的唤醒等待的操作就一定会出现在有元素入队列的时候,在put()
方法的最后调用了enqueue()
方法来将元素入队列,下面看一下其实现。
private void enqueue(E x) {
final Object[] items = this.items;
// 本次入队列的元素添加到items数组的putIndex位置
items[putIndex] = x;
if (++putIndex == items.length)
// 本次入队列的元素添加到了items数组最后一个位置
// 则下一次入队列的元素需要添加到items数组第一个位置
putIndex = 0;
// 本次元素入队列后,阻塞队列元素个数加1
count++;
// 因为发生了元素入队列,此时阻塞队列一定不为空
// 那么将在notEmpty上第一个等待的线程唤醒
notEmpty.signal();
}
和dequeue()
方法相似,enqueue()
方法中使用了一个叫做putIndex的整型变量,该变量默认情况下初始值为0,作为元素数组items的下标索引使用,每次元素入队列时,入队列的元素会被添加到items数组的putIndex位置,所以可以理解为putIndex永远指向队列尾。此外,调用enqueue()
方法的线程也一定持有锁,并且元素入队列后阻塞队列一定不为空,所以enqueue()
方法最后还会调用notEmpty的signal()
方法,唤醒第一个因为调用take()
方法时队列为空而在notEmpty上等待的线程。
总结
ArrayBlockingQueue
在创建时需要指定初始容量;ArrayBlockingQueue
持有一把重入锁,ArrayBlockingQueue
中的元素入队列的操作add()
,offer()
和put()
,以及ArrayBlockingQueue
中的元素出队列的操作remove()
,poll()
和take()
,全都会进行加锁;ArrayBlockingQueue
持有一对Condition
,分别为notFull和notEmpty,当ArrayBlockingQueue
已满且有线程调用put()
方法,此时调用put()
方法的线程会在notFull上等待,当ArrayBlockingQueue
为空且有线程调用take()
方法,此时调用take()
方法的线程会在notEmpty上等待;- 当有元素出队列后,会唤醒第一个在notFull上等待的线程,当有元素入队列后,会唤醒第一个在notEmpty上等待的线程;
ArrayBlockingQueue
使用一个Object
数组items来存放元素,同时还有两个整型变量分别叫做takeIndex和putIndex,默认情况下初始值均为0,takeIndex标识队列头元素在items数组中的位置,putIndex标识新入队列的元素应该被添加到items数组中的哪个位置,即takeIndex一直指向队列头,putIndex一直指向队列尾。