开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 9 天,点击查看活动详情
大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实,最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈
前言
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一直指向队列尾。
大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实,最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 9 天,点击查看活动详情