ArrayBlockingQueue
ArrayBlockingQueue 是基于数组的阻塞队列。
关于阻塞队列,我们首先应该阅读BlockingQueue接口,在这个接口中可以大致了解阻塞队列都有哪些功能。能更加容易的理解阻塞队列的作用。
继承关系
- Iterable保证有迭代的功能
- Collection,AbstractCollection说明ArrayBlockQueue也是具有普通集合的功能
- Queue,AbstractQueue接口更近一步标识它是一个队列,且具有队列的功能
- BlockingQueue表明它是一个阻塞队列
源码解析
本篇源码解析,只分享最核心的部分:主要属性,初始化,新增,取出,删除。
主要属性
解释都在代码块的注释里面
// 重要属性
/**
* 队列存放在 object 的数组里面
* 数组大小必须在初始化的时候手动设置,没有默认大小
*/
final Object[] items;
/**
* 下次拿数据的时候的索引位置
*/
int takeIndex;
/**
* 下次放数据的索引位置
*/
int putIndex;
/**
* 队列中数据的个数
*/
int count;
//并发控制属性
/**
* 可重入锁,控制并发插入和获取
*/
final ReentrantLock lock;
/**
* take队列,只有在阻塞队列不是空的时候,才可以进行take,否则take阻塞
*/
private final Condition notEmpty;
/**
* put队列,只有在阻塞队列不满的时候,才可以进行put,否则put阻塞
*/
private final Condition notFull;
初始化方式
ArrayBlockQueue的初始化必须指定容量
// 初始化
/**
* 初始化方法,基于数组的阻塞队列的初始化方法没有无参的,只有有参构造。
* 且一开始初始化必须指定大小。没有默认大小。
*/
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
/**
* 初始化方法
*
* @param capacity 初始化必须指定容量。没有默认大小
* @param fair 公平还是非公平,内部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();
}
/**
* 初始化方法
*
* @param capacity 初始化必须指定容量。没有默认大小
* @param fair 公平还是非公平,内部ReentrantLock使用
* @param c 初始化元素值,元素必须少于数组容量,否则抛异常
*/
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();
}
}
核心操作方法
新增
- 数据新增都会按照 putIndex 的位置进行新增
/**
* 队列新增数据
* 新增,如果队列满,无限阻塞
*
* @param e 新增的元素
* @throws InterruptedException
*/
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();
}
}
/**
* 元素入队核心操作
*
* @param x 元素
*/
private void enqueue(E x) {
final Object[] items = this.items;
// putIndex表示本次插入的位置
items[putIndex] = x;
// 插入完之后要比较一下是否到队尾了,如果已经在队尾了,那么下一次新增
// 新增的位置为队列头,也就是数组下标为0的位置
if (++putIndex == items.length)
putIndex = 0;
// 队列元素个数 +1
count++;
// 唤醒一个因为队列空导致的等待线程
notEmpty.signal();
}
从源码中,可以看出,其实新增就两种情况:
- 本次新增的位置居中,直接新增,下图演示的是 putIndex 在数组下标为 5 的位置,还不到队尾,那么可以直接新增,计算下次新增的位置应该是 6;
- 新增的位置到队尾了,那么下次新增时就要从头开始了,示意图如下:
上面这张图演示的就是这行代码:
if (++putIndex == items.length) putIndex = 0;
可以看到当新增到队尾时,下次新增会重新从队头重新开始。
取数据
- 拿数据都是从队头开始拿数据
/**
* 拿数据操作
* 从队头开始拿数据
*
* @return
* @throws InterruptedException
*/
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 如果队列为空,无限等待
// 直到队列中有数据被 put 后,自己被唤醒
while (count == 0)
notEmpty.await();
// 唤醒之后,进行出队操作
return dequeue();
} finally {
// 释放锁
lock.unlock();
}
}
/**
* 出队操作
*/
private E dequeue() {
final Object[] items = this.items;
// 直接获取下一个可以出队的位置的元素
E x = (E) items[takeIndex];
// 将出了队的元素位置的引用置为null,帮助GC进行垃圾回收
items[takeIndex] = null;
// 计算下一次出队的位置,如果已经到了队尾,那么下一个出队的就是下标为0的元素
if (++takeIndex == items.length)
takeIndex = 0;
// 队列元素个数 -1
count--;
if (itrs != null)
itrs.elementDequeued();
// 唤醒被队列满所阻塞的线程
notFull.signal();
return x;
}
从源码中可以看出,每次拿数据的位置就是 takeIndex 的位置,在找到本次该拿的数据之后,会把 takeIndex 加 1,计算下次拿数据时的索引位置,有个特殊情况是,如果本次拿数据的位置已经是队尾了,那么下次拿数据的位置就要从头开始,就是从 0 开始了。
删除数据
删除数据有两种情况:
- 删除位置和 takeIndex 的关系:删除位置和 takeIndex 一样,比如 takeIndex 是 2, 而要删除的位置正好也是 2,那么就把位置 2 的数据置为 null ,并重新计算 takeIndex 为 3。
- 找到要删除元素的下一个,计算删除元素和 putIndex 的关系
- 如果下一个元素不是 putIndex,就把下一个元素往前移动一位
- 如果下一个元素是 putIndex,把 putIndex 的值修改成删除的位置
/**
* 删除
*/
void removeAt(final int removeIndex) {
final Object[] items = this.items;
// 删除位置和 takeIndex 一样,也就是要删除的元素和要取出的元素是一样的。
// 这种情况下,现将要删除的元素删除掉,然后重新计算一个takeIndex(取出的位置)
if (removeIndex == takeIndex) {
// 要删除的元素置为null
items[takeIndex] = null;
// 计算下一个可以取出元素的位置
if (++takeIndex == items.length)
takeIndex = 0;
// 元素个数 -1
count--;
if (itrs != null)
itrs.elementDequeued();
} else {
// 否则要看待删除的元素位置和待插入的元素的位置关系
// 如果下一个元素不是 putIndex,就把下一个元素往前移动一位
// 如果下一个元素是 putIndex,把 putIndex 的值修改成删除的位置
final int putIndex = this.putIndex;
for (int i = removeIndex; ; ) {
int next = i + 1;
if (next == items.length)
next = 0;
// 如果下一个元素不是 putIndex,就把下一个元素往前移动一位
if (next != putIndex) {
items[i] = items[next];
i = next;
} else {
//如果下一个元素是 putIndex,把 putIndex 的值修改成删除的位置
items[i] = null;
this.putIndex = i;
break;
}
}
count--;
if (itrs != null)
itrs.removedAt(removeIndex);
}
notFull.signal();
}
图示
- 第一种情况是 takeIndex == removeIndex
-
第二种情况分为两种
-
如果 removeIndex + 1 != putIndex 的话,就把下一个元素往前移动一位,示意图如下
-
如果 removeIndex + 1 == putIndex 的话,就把 putIndex 的值修改成删除的位置,示意图如下:
-
总结
ArrayBlockQueue就先总结这么点知识。回顾它的几个重点:
- 有界数组作为底层数据结构
- 当 takeIndex、putIndex 到队尾的时候,都会重新从 0 开始循环。(相当于是一个环)