阅读须知
- JDK版本:1.8
- 文章中使用/* */注释的方法会做深入分析
正文
ArrayBlockingQueue,命名上就能看出其含义,基于数组的 FIFO 阻塞队列,由 Doug Lea 大神开发,在 jdk1.5 版本跟我们见面,关于阻塞队列的应用这里不多赘述,相信大家都有所了解,我们直接来看源码实现。首先我们来看内部的成员变量:
ArrayBlockingQueue:
// 用于存储队列内部的元素
final Object[] items;
// 下一个 take、poll、peek、remove 操作的元素索引
int takeIndex;
// 下一个 put、offer、add 操作的元素索引
int putIndex;
// 队列中元素的数量
int count;
// 所有访问入口的锁
final ReentrantLock lock;
// 出队操作的等待条件
private final Condition notEmpty;
// 入队操作的等待条件
private final Condition notFull;
// 当前活动迭代器的共享状态,如果没有,则为 null,允许队列操作更新迭代器状态
transient Itrs itrs = null;
下面我们来看一下构造方法:
ArrayBlockingQueue:
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
// 初始化队列数组,capacity 为容量
this.items = new Object[capacity];
// fair 为公平或非公平锁的标记
lock = new ReentrantLock(fair);
// 初始化入队和出队操作的等待条件
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
下面我们来看入队和出队相关的几个方法的实现,首先我们来看入队操作:
ArrayBlockingQueue:
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
// 如果元素的数量已经和数组的长度相等,证明队列已满,返回 false 代表入队失败
return false;
else {
/* 入队操作 */
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
ArrayBlockingQueue:
private void enqueue(E x) {
final Object[] items = this.items;
// 将元素放在数组的 putIndex 下标对应的位置
items[putIndex] = x;
if (++putIndex == items.length)
// 如果 putIndex 自增后与数组的长度相等,说明这次入队操作的元素已经放到数组的最后一个位置,所以重置 putIndex 为0,下一个元素入队后从下标为0开始存储
putIndex = 0;
// 元素成功入队后将当前队列中元素的数量自增1
count++;
// 唤醒等待非空条件的线程,表示当前队列中有元素了,可以获取了
notEmpty.signal();
}
不知道大家是否有疑问,这里如果将 putIndex 重置为0之后,下一个入队的元素就会放在0这个位置,如果这个位置的元素还没有被取出,那不就覆盖了么?其实并不会发生覆盖,ArrayBlockingQueue 为 FIFO 队列,所以元素会按顺序进行入队和出队,在 offer 方法的开始校验了count == items.length
,所以如果0下标的元素没有被取出,这个判断就会为 true 代表数组已经满了,这时 offer 方法返回 false 代表插入失败,所以并不会有覆盖的情况发生,这同时也说明了 offer 方法是不会阻塞的。
还有两个入队的方法 add 和 put,add 方法最终调用了 offer 方法,所以流程是一样的,我们来看另一个入队方法 put:
ArrayBlockingQueue:
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
// 如果元素的数量已经和数组的长度相等,证明队列已满,将线程阻塞在 notFull 条件上等待唤醒
notFull.await();
// 入队操作
enqueue(e);
} finally {
lock.unlock();
}
}
put 方法和 offer 方法唯一的区别就是在于对数组元素已满情况下的处理,offer 方法会直接返回 false 代表失败,而 put 方法会将当前线程阻塞在 notFull 条件上等待唤醒,所以我们猜测,在出队操作中一定有对等待在 notFull 条件上的线程的唤醒操作,我们来看出队方法的实现:
ArrayBlockingQueue:
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
/* 如果数组中的元素为空,返回 null,不为空做出队操作 */
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
ArrayBlockingQueue:
private E dequeue() {
final Object[] items = this.items;
// 取出 takeIndex 对应的元素
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
// 将 takeIndex 位置的元素置为 null
items[takeIndex] = null;
if (++takeIndex == items.length)
// 如果出队索引自增后与数组的长度相等,说明本次出队操作已经取到了队尾,将 takeIndex 重置为0,下次出队操作从0开始取
takeIndex = 0;
// 出队成功,减少数组中元素的数量
count--;
if (itrs != null)
// 每当一个元素出队时调用
itrs.elementDequeued();
// 唤醒在 notFull 条件上等待的线程
notFull.signal();
return x;
}
下面我们来看另外几个出队方法,peek 方法直接获取数组 takeIndex 下标的元素,不会将下标位置的元素置为 null,我来看 take 方法:
ArrayBlockingQueue:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
// 如果元素的数量为0,证明队列已空,将线程阻塞在 notEmpty 条件上等待唤醒
notEmpty.await();
// 出队操作
return dequeue();
} finally {
lock.unlock();
}
}
同样的,与 poll 方法的区别就是在于数组元素为空时的处理,poll 方法会返回 null,take 方法会阻塞。poll 方法还有一个重载的实现,这个方法可以指定等待的时间:
ArrayBlockingQueue:
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
// 转换时间单位
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 同样的数组元素个数为0的判断
while (count == 0) {
if (nanos <= 0)
return null;
// 设置超时时间的阻塞
nanos = notEmpty.awaitNanos(nanos);
}
// 出队操作
return dequeue();
} finally {
lock.unlock();
}
}
ArrayBlockingQueue:
public boolean remove(Object o) {
if (o == null) return false;
final Object[] items = this.items;
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 数组元素数量大于0移除操作才有意义
if (count > 0) {
final int putIndex = this.putIndex;
// 从 takeIndex 开始遍历
int i = takeIndex;
do {
if (o.equals(items[i])) {
// equals 判断为 true,则移除对应位置的元素,返回成功
removeAt(i);
return true;
}
if (++i == items.length)
// 自增后与数组长度比较,如果相等,证明已经遍历到数组的最后一个下标,重置为0再次遍历
i = 0;
// 如果待移除的索引值与 putIndex 相等,结束循环,说明没有找到匹配的元素
} while (i != putIndex);
}
return false;
} finally {
lock.unlock();
}
}
到这里,入队和出队操作就分析完了。我们在分析线程池源码的时候看到过,我们在使用 shutdownNow 方法停止线程池的时候,会调用 BlockingQueue 的 drainTo 方法移除此队列中所有可用的元素,并将它们添加到给定 collection 中,我们来看一下ArrayBlockingQueue对相关方法的实现:
public int drainTo(Collection<? super E> c, int maxElements) {
checkNotNull(c);
if (c == this)
throw new IllegalArgumentException();
// maxElement 是想要移除的元素的数量
if (maxElements <= 0)
return 0;
final Object[] items = this.items;
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 比较 maxElements(另一个重载的 drainTo 方法会将此值传为 Integer.MAX_VALUE)和元素的数量,取较小的一个
int n = Math.min(maxElements, count);
// 从 takeIndex 开始
int take = takeIndex;
int i = 0;
try {
// n - i 就是要操作的元素的个数
while (i < n) {
@SuppressWarnings("unchecked")
E x = (E) items[take];
// 将元素添加到给定集合中
c.add(x);
// 将数组的对应下标位置置为 null
items[take] = null;
if (++take == items.length)
// 如果已经遍历到数组的最后一个位置,重置为0
take = 0;
i++;
}
return n;
} finally {
if (i > 0) {
// count - i 就是剩余元素的数量
count -= i;
// 将 takeIndex 置为当前取到的下标
takeIndex = take;
if (itrs != null) {
if (count == 0)
// 通知所有活动迭代器队列为空,清除所有弱引用,并且断开它的数据结构
itrs.queueIsEmpty();
else if (i > take)
// i > take 说明 take 发生了重置为0的情况,这时要通知所有迭代器,并删除所有旧的迭代器
itrs.takeIndexWrapped();
}
for (; i > 0 && lock.hasWaiters(notFull); i--)
notFull.signal();
}
}
} finally {
lock.unlock();
}
}
到这里,ArrayBlockingQueue 的源码解析就完成了。