从类注释可以得到的信息如下:
- 有界的阻塞数组,容量一旦创建,后续大小无法修改;
- 元素是有顺序的,按照先入先出进行排序,从队尾插入数据数据,从队头拿数据;
- 队列满时,往队列中 put 数据会被阻塞,队列空时,往队列中拿数据也会被阻塞。
1.结构
ArrayBlockingQueue 的继承关系,核心成员变量及主要构造函数:
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
// 保存数据的数组,ArrayBlockingQueue 实际上是通过数组实现了一个循环队列
// 注:数组大小必须在初始化的时候手动设置,没有默认大小
final Object[] items;
// 下次拿数据的时候的索引位置(队首)
int takeIndex;
// 下次放数据的索引位置(队尾)
int putIndex;
// 队列中已有元素的个数(<=length)
int count;
/**
* 注:takeIndex和putIndex都是动态变化的,因为数组容量(length)初始化好就始终不会再变,主要的变化为两种:
* 1.在元素入队后putIndex++,在元素出队后takeIndex++
* 2.在putIndex或者takeIndex == length-1(到达数组末端),且count != length(队列未满)时,它俩会被重置为0
* /
// 可重入的锁,作用有两个
// 1.保证在并发时对数组操作(入队/出队)的线程安全。另外,还可以设置是否公平锁
// 2.通过条件队列(Condition)实现线程控制
final ReentrantLock lock;
// 入队(put)的条件队列,其实很容易理解:入队条件是队列未满
private final Condition notFull;
// 出队(take)的条件队列。其实很容易理解:出队条件是队列非空
private final Condition notEmpty;
//----------------------------------构造函数--------------------------------
// 构造函数一:传入初始容量
// 注:ArrayBlockingQueue 无空参构造
public ArrayBlockingQueue(int capacity) {
// 默认是非公平锁
this(capacity, false);
}
// 构造函数二:传入初始容量和是否公平锁
public ArrayBlockingQueue(int capacity, boolean fair) {
// 容量参数合法判断
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
// 创建锁
// 1.公平,即按照阻塞的先后顺序唤醒;
// 2.非公平(默认),未阻塞或阻塞时间少的线程有机会先获得锁
lock = new ReentrantLock(fair);
// 初始化两个条件队列
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
// 构造函数三:传入一个集合
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 {
// i 代表插入的位置
int i = 0;
try {
// 此时需要注意的是,如果 c 的大小超过了数组的大小
// 是会抛异常的
for (E e : c) {
checkNotNull(e);
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
// 如果插入的位置,正好是队尾了,下次需要从 0 开始插入
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
}
2.队列的继承关系
在讲 ArrayBlockingQueue 的方法源码之前,我们先来看一下 BlockingQueue 的继承关系。下图是 ArrayBlockingQueue 的类图:
2.1 Queue:队列的顶层接口
public interface Queue<E> extends Collection<E> {
// 入队
boolean add(E e);
boolean offer(E e);
// 出队
E remove();
E poll();
// 队首
E peek();
E element();
}
2.2 AbstractQueue:中间层
- 所有的Queue不是直接实现Queue,而是直接继承AbstractQueue
- AbstractQueue实现了部分方法
- add:对offer进行了封装,若offer为false,则抛异常
- remove:对poll进行了封装,若poll为null,则抛异常
- element:对peek进行了封装,若peek为null,则抛异常
- 实现了clear,清空队列
public abstract class AbstractQueue<E>
extends AbstractCollection<E>
implements Queue<E> {
public boolean add(E e) {
// offer
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
public E element() {
E x = peek();
// peek
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public E remove() {
E x = poll();
// poll
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public boolean addAll(Collection<? extends E> c) {
if (c == null)
throw new NullPointerException();
if (c == this)
throw new IllegalArgumentException();
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
public void clear() {
// 将所有元素弹出
while (poll() != null)
;
}
}
2.3 BlockQueue:阻塞队列
阻塞队列的顶层接口,继承了Queue接口
public interface BlockingQueue<E> extends Queue<E>{
boolean add(E e);
// 注:子类调用remove方法时会调用AbstractQueue#remove()
void put(E e) throws InterruptedException;
E take() throws InterruptedException;
// 阻塞一定时间
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
E poll(long timeout, TimeUnit unit) throws InterruptedException;
// 删除指定与安娜
boolean remove(Object o);
//......
}
在阻塞队列中,上面三组出队入队方法的对比如下:
抛异常 | 返回特殊值 | 阻塞(WAITING) | 阻塞一段时间 | |
---|---|---|---|---|
入队(满) | add | offer(false) | put | offer 过超时时间返回 false |
出队(空) | remove | poll (null) | take | poll 过超时时间返回 null |
队首(空) | element | peek (null) | 暂无 | 暂无 |
- 这些方法保证线程安全的方法都是通过锁,因此在夺锁过程中都可能发生阻塞
- 区别只是在碰见空(满)会让相应线程进入WAITING
3.方法解析 & api
从上一部分中,我们看到了队列的主要方法无非就三个:入队、出队、队首。所以接下来我们就从这三个方法入手,来看看 ArrayBlockingQueue 的具体实现。
3.1 入队
put():队满阻塞
public void put(E e) throws InterruptedException {
checkNotNull(e); // 入队元素不能为空
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 上锁,设置为可中断
try {
// !!!队列如果是满的,就将当前线程加入到notFull的条件队列中,然后进入阻塞状态
// 等待某个线程执行拿走队列元素(dequeue)或者删除元素(removeAt)后,有机会被唤醒进入同步队列
// 注:这里的while循环有double-check的意思,即防止线程已经被调度执行了,但前一刻有另一个线程put/offer/add成功了,所以他又要进入条件队列notFull中阻塞等待。
while (count == items.length)
notFull.await();
enqueue(e); // 入队时数组的相关操作
} finally {
lock.unlock(); // 释放锁
}
}
这里面涉及到的是AQS相关源码,感兴趣的童鞋可以参考JUC核心:AQS源码最详细万字深析(同步队列&条件队列)
offer():队满返回false
public boolean offer(E e) {
checkNotNull(e); // 入队元素不能为空
final ReentrantLock lock = this.lock;
lock.lock(); // 加锁
try {
// !!!如果队列是满的,直接返回 false
if (count == items.length)
return false;
else {
enqueue(e); // 入队时数组的相关操作
return true;
}
} finally {
lock.unlock(); // 释放锁
}
}
add():队满抛异常
public boolean add(E e) {
// !!!直接调用offer方法,若失败,就抛异常
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
enqueue()
入队时数组的相关操作:
- 获取原数组
- 在 putIndex(队尾) 位置放入元素
- 计算下一次 putIndex(队尾) ,若已经到数组末尾就将 putIndex 置为队首
- 随机唤醒一个出队时阻塞在 notEmpty 条件队列的线程
private void enqueue(E x) {
// assert lock.getHoldCount() == 1; 同一时刻只能一个线程进行操作此方法
// assert items[putIndex] == null;
final Object[] items = this.items;
// putIndex 为本次插入的位置
items[putIndex] = x;
// 计算下次putIndex位置,若到最后了就将index置为0
// 注:保证不会覆盖队首的前提条件是,count==length时在上面三个方法就已经阻塞或返回了
if (++putIndex == items.length)
putIndex = 0;
count++;
// 唤醒取出线程
notEmpty.signal();
}
3.2 出队
take():队空阻塞
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 加锁,设置为可中断
try {
// !!!队列如果是空的,就将当前线程加入到notEmpty条件队列中,然后进入阻塞状态
// 等待某个线程拿向队列放入元素(enqueue)后,有机会被唤醒进入同步队列
// 注:这里的while循环有double-check的意思,即防止线程已经被调度执行了,但前一刻有另一个线程take/poll/remove成功了,所以他又要进入条件队列notFull中阻塞等待。
while (count == 0)
notEmpty.await();
return dequeue(); // 出队时数组的相关操作
} finally {
lock.unlock(); // 释放锁
}
}
poll():队空返回null
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock(); // 加锁
try {
// !!!队列为空,直接返回null
return (count == 0) ? null : dequeue(); // 出队时数组的相关操作
} finally {
lock.unlock(); // 释放锁
}
}
remove():队空抛异常
思路同上面的add方法,都是直接调用offer/poll,但在 ArrayBlockingQueue 中没有直接实现该方法,而是在它的父类 AbstractQueue 中:
dequeue()
出队时数组的相关操作:
- 获取原数组
- 将 takeIndex(队首) 位置的元素删除(置为null)
- 计算下一次 takeIndex(队首),若已经到达数组末尾就将 takeIndex 置为队首
- 随机唤醒一个入队时阻塞在 notFull 条件队列的线程
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
// takeIndex 代表本次拿数据的位置,是上一次拿数据时计算好的
E x = (E) items[takeIndex];
// 帮助 gc
items[takeIndex] = null;
// ++ takeIndex 计算下次拿数据的位置,若idx=length,idx=0
if (++takeIndex == items.length)
takeIndex = 0;
// 队列实际大小减 1
count--;
if (itrs != null)
itrs.elementDequeued();
// 唤醒要放入的线程
notFull.signal();
return x;
}
3.3 获取队首:peek
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock(); // 加锁
try {
// 循环队列,返回takeIndex位置的元素
return itemAt(takeIndex);
} finally {
lock.unlock(); // 释放锁
}
}
3.4 删除
remove():删除指定元素
这个方法其实就是寻找要删除元素的索引,然后调用 removeAt 方法执行删除
// 传入要删除的元素,返回删除成功或者失败
public boolean remove(Object o) {
if (o == null) return false;
final Object[] items = this.items;
final ReentrantLock lock = this.lock;
lock.lock(); // 加锁
try {
// 判断队列中是否有元素,若为空则放回false
if (count > 0) {
final int putIndex = this.putIndex;
int i = takeIndex;
// do-while循环寻找要删除元素的索引,然后调用removeAt方法执行删除
do {
if (o.equals(items[i])) {
removeAt(i);
return true;
}
// !!重点:如果到数组末尾了,就将i置为0
if (++i == items.length)
i = 0;
} while (i != putIndex);
}
return false;
} finally {
lock.unlock(); // 释放锁
}
}
removeAt():删除指定位置
删除指定idx的元素,这里分为了两种情况:
- 情况一:removeIndex == takeIdx
- 将removeIndex的元素置为null,表示删除了指定元素
- takeIdx后移(++),表示下次直接取下一个元素
- 情况二:removeIndex != takeIdx,这里又分为两种情况:
- removeIndex+1 == putIdx,直接将putIdx前移一位
- removeIndex+1 != putIdx,通过for循环将removeIndex与putIdx间元素前移一位,直到removeIndex+1 == putIndex时执行上面情况一
- removeIndex+1 == putIdx,直接将putIdx前移一位
void removeAt(final int removeIndex) {
final Object[] items = this.items;
// 情况一: 删除位置正好等于下次要拿数据的位置
if (removeIndex == takeIndex) {
// 下次要拿数据的位置直接置空
items[takeIndex] = null;
// 要拿数据的位置往后移动一位
if (++takeIndex == items.length)
takeIndex = 0;
// 当前数组的大小减一
count--;
if (itrs != null)
itrs.elementDequeued();
// 情况二:删除位置不等于下次要拿数据的位置
} else {
final int putIndex = this.putIndex;
for (int i = removeIndex;;) {
// 找到要删除元素的下一个
int next = i + 1;
if (next == items.length)
next = 0;
// 2.1 下一个元素不是 putIndex
if (next != putIndex) {
// 下一个元素往前移动一位
items[i] = items[next];
i = next;
// 2.2 下一个元素是 putIndex
} else {
// 删除元素
items[i] = null;
// 下次放元素时,应该从本次删除的元素放
this.putIndex = i;
break;
}
}
count--;
if (itrs != null)
itrs.removedAt(removeIndex);
}
notFull.signal();
}