JUC 之队列 ArrayBlockingQueue 详解
通过类名我们就可以知道是通过数组实现的 BlockingQueue 阻塞队列。既然已经猜到了底层实现,那么我就开始进入其源码一探究竟,看看它是如何通过数组实现的阻塞队列。首先我们先来看看怎么用,这里给一个简单示例:
public static void main(String[] args) throws Exception {
ArrayBlockingQueue<Integer> abq = new ArrayBlockingQueue<>(2);
abq.add(1);
abq.offer(2);
abq.put(3);// put 方法在队列满时会阻塞
Integer take = abq.take();// take 方法在队列空时阻塞
Integer poll = abq.poll();
}
ArrayBlockingQueue 使用方式很简单,后面我要重点关注的是它是如何实现 put 与 take 的阻塞操作的。
核心成员介绍
// 由于不知道具体要存放的数据类型,所以此处声明为 Object 类型的数组
final Object[] items;
// 指向下一个要出队的元素索引下标
int takeIndex;
// 指向下一个存放元素的索引下标
int putIndex;
// 记录数组中的元素个数
int count;
// 控制多线程并发操作的安全性
final ReentrantLock lock;
// 数组空时阻塞获取线程的条件变量
private final Condition notEmpty;
// 数组满时阻塞添加线程的条件变量
private final Condition notFull;
// 在构造器中初始化数组项,锁信息,条件信息
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();
}
核心方法讲解
offer 方法
分两种实现,一种不带超时等待的添加操作,一种带有超时等待的添加操作,具体源码如下,而 add 方法最终也是调用该方法完成最终的添加操作。
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();// 上锁,此处可以直接使用 this.lock.lock() 方式上锁
try {
if (count == items.length)// 校验数组时候已满
return false;
else {
enqueue(e);// 实际添加操作
return true;
}
} finally {
lock.unlock();
}
}
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);
long nanos = unit.toNanos(timeout);// 计算要阻塞等待的时间
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);// 调用 Condition 的 awaitNanos 方法阻塞等待指定时间
}
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
enqueue 方法
元素添加队列操作,通过 putIndex 索引完成操作。添加成功后调用 notEmpty.signal() 唤醒阻塞的 take 获取数据的线程,唤醒流程可参考【AQS 之 Condition 源码剖析】一篇的讲解。
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;// 将数据放入 putIndex 索引下标处
if (++putIndex == items.length)// 数组满了,将 putIndex 置 0
putIndex = 0;
count++;// 计数加 1
notEmpty.signal();// 唤醒阻塞线程
}
put 方法
通过 count 计数器校验数组是否已满,满了调用 notFull.await() 方法进行阻塞,否则调用 enqueue 方法将数据添加到队列中,阻塞流程可参考【AQS 之 Condition 源码剖析】一篇的讲解。由于 put 方法会响应中断,所以在获取锁时调用的是 ReentrantLock 的 lockInterruptibly方法,该方法的实现可参考【JUC 之 ReentrantLock 源码解析】一篇的中的 lock 方法的讲解,两个方法的区别就在于 lock 方法不响应中断,而 lockInterruptibly 方法响应中断,其他实现都一样。这两个方法最终会调用 AQS 中 acquire 方法和 acquireInterruptibly 方法
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); // 调用 enqueue 方法将数据添加到队列中
} finally {
lock.unlock();
}
}
poll 方法
从队列中获取数据,通过 takeIndex 索引实现获取操作。
public E poll() {// 队列为空时直接返回 null
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();// 不为空时 调用 dequeue 方法完成获取操作
} finally {
lock.unlock();
}
}
// 队列为空时阻塞等待指定时间,还是没有数据返回 null,阻塞阶段可被中断唤醒
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) {
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);// 调用 Condition 的 awaitNanos 方法阻塞等待指定时间
}
return dequeue();// 不为空时 调用 dequeue 方法完成获取操作
} finally {
lock.unlock();
}
}
dequeue 方法
实际获取操作,通过 takeIndex 从数组中取出该索引下标处的元素并从队列中删除该数据,取出数据后唤醒阻塞在 notFull 条件上的添加数据的线程(也即生产数据的线程)。
private E dequeue() {
final Object[] items = this.items;
E x = (E) items[takeIndex];
items[takeIndex] = null;// 获取到数据后,清空数组中该下标处的值
if (++takeIndex == items.length)
takeIndex = 0; // 队列中没有数据了
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();// 唤醒操作
return x;
}
peek 方法
通过 takeIndex 从数组中取出该索引下标处的元素,不会从队列中删除该数据,这是与 poll 方法不同的地方。当队列为空时,peek() 方法将返回 null
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return itemAt(takeIndex); // null when queue is empty
} finally {
lock.unlock();
}
}
final E itemAt(int i) {
return (E) items[i];
}
take 方法
队列为空时,调用 Condition 的 await 方法进行阻塞等待,否则调用 dequeue 从队列中获取数据,该方法可响应中断
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();// 队列中没有数据,阻塞等待
return dequeue();// 获取数据
} finally {
lock.unlock();
}
}
remove 方法
从队列中删除指定元素。从 takeIndex 处遍历比较数据是否是要删除的数据,然后调用 removeAt 方法完成删除操作
public boolean remove(Object o) {
if (o == null) return false;
final Object[] items = this.items;
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count > 0) {
final int putIndex = this.putIndex;
int i = takeIndex;// 从 takeIndex 处开始遍历队列中的数据
do {
if (o.equals(items[i])) {// 数据校验
removeAt(i);// 删除操作
return true;
}
if (++i == items.length)
i = 0;
} while (i != putIndex);
}
return false;
} finally {
lock.unlock();
}
}
void removeAt(final int removeIndex) {
final Object[] items = this.items;
if (removeIndex == takeIndex) {// 要删除的数据就存放在 takeIndex 处,直接删除即可,将 takeIndex 索引处置空
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
} else {// 要删除的数据不在 takeIndex 处
final int putIndex = this.putIndex;
for (int i = removeIndex;;) {// 从 removeIndex 处开始循环遍历将其后面的数据向前移动,然后将最后putIndex处的数据置空(因为 putIndex 中的数据已经向前移动到了前一个索引下标处了,也即 putIndex - 1 处),最后更新 putIndex 索引值
int next = i + 1;
if (next == items.length)
next = 0;
if (next != putIndex) {
items[i] = items[next];
i = next;
} else {
items[i] = null;
this.putIndex = i;
break;
}
}
count--;
if (itrs != null)
itrs.removedAt(removeIndex);
}
notFull.signal();
}
总结:上述就是我们在使用 ArrayBlockingQueue 时的常用方法了,通过上述学习我们很容易发现,通过 ArrayBlockingQueue 我们可以很方便的就能实现一个生产者消费者模型。通过一个 ReentrantLock 和两个 Condition 条件变量 notEmpty(队列空时阻塞消费者线程)、notFull(当队列满了阻塞生产者线程)来实现。