前言
ArrayBlockingQueue是一种FIFO(first-in-first-out 先入先出)的有界阻塞队列,底层是数组,也支持从内部删除元素。并发操作依赖于加锁的控制,支持阻塞式的入队出队操作。正因为有界,所以才会阻塞。
加锁实现完全依赖于AQS,需要读者比较熟悉AQS 独占锁的获取过程和AQS Condition接口的实现。对ArrayBlockingQueue的源码解析,更像是了解一次AQS的最佳实践。
成员
//保存队列元素的数组
final Object[] items;
//下次出队的位置
int takeIndex;
//下次入队的位置
int putIndex;
//队列中元素的数量
int count;
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
队列中非null元素的范围是[takeIndex, putIndex)
的左闭右开的区间。考虑到底层是循环数组,有可能putIndex
比takeIndex
小。二者相等也很好理解,代表队列中每个元素都是非null元素。
术语:
队列:指ArrayBlockingQueue本身。
同步队列:指AQS的sync queue
。
条件队列:指AQS的condition queue
。
构造器
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
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();
}
构造器默认使用的是非公平的ReentrantLock,当然你也可以指定为公平的ReentrantLock。
public ArrayBlockingQueue(int capacity, boolean fair,
Collection<? extends E> c) {
this(capacity, fair);
final ReentrantLock lock = this.lock;
lock.lock(); // 加锁只是为了保证可见性
try {
int i = 0;
try {
for (E e : c) {
checkNotNull(e);
items[i++] = e;//如果传入集合的个数超过了容量,抛出异常被catch,最多放capacity个
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;//循环结束,i刚好是放置的个数
putIndex = (i == capacity) ? 0 : i;//循环结束,i也刚好是最后放置元素的索引+1
} finally {
lock.unlock();
}
}
如果传入集合的个数超过了容量,抛出异常被catch,最多放capacity个元素。
入队
add
//ArrayBlockingQueue.java
public boolean add(E e) {
return super.add(e);
}
//AbstractQueue.java
public boolean add(E e) {
if (offer(e))
return true;
else//返回false的处理不一样
throw new IllegalStateException("Queue full");
}
//Queue.java(接口文件)
boolean offer(E e);
add
的实现是依靠父类的add
实现,后者又依靠于子类的offer
实现。所以,add
就是在调用自己的offer
方法,只不过有点绕。
offer
private static void checkNotNull(Object v) {
if (v == null)
throw new NullPointerException();
}
public boolean offer(E e) {
checkNotNull(e);//不能加入空元素,否则空指针异常
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
- 入队是一个写操作,自然需要加锁。
lock.lock()
不响应中断,线程会一直阻塞直到抢到锁。 - 队列已满,则无法入队,返回false。
- 队列未满,则可以入队,返回true。
private void enqueue(E x) {
// assert lock.getHoldCount() == 1; 即保证,调用此函数的线程已经获得锁
// assert items[putIndex] == null; 即保证,入队位置是空的
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)//更新putIndex
putIndex = 0;
count++;//大小加1
notEmpty.signal();//队列不为空的条件,已经满足。
}
- 在putIndex位置是空的,我们直接往putIndex索引上入队。
- 右移putIndex,按照循环数组的方式。
- 队列大小加1。
- 通知沉睡在notEmpty条件队列上的线程,只通知一个线程。
put
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();
}
}
- 在进入加锁代码之前,执行的是
lock.lockInterruptibly()
。这意味着,当前线程在抢到锁之前,如果被中断了,put
方法会抛出中断异常。 - 进入加锁代码之后,当前线程便已是获得了锁。但获得了锁,和队列当前是空是满根本没有关系。
- 如果队列未满,那么根本不会执行
notFull.await()
,直接入队。 - 需要使用
while (count == items.length)
来防止虚假唤醒,即使当前线程从notFull.await()
恢复执行了,如果当前队列还是满的,那么应该重新进入条件队列。所以,需要重新检查一遍count == items.length
。- 你可能会产生疑问,为什么需要重新检查一遍。因为当前线程从
notFull.await()
恢复执行,一定是因为别的线程执行了notFull.signal()
(别的线程的这个时间点,队列确实未满)。但由于当前线程是从AQS的条件队列转移到AQS的同步队列的队尾,而排在同步队列前面的其他线程也有可能去执行入队操作,可能等到当前线程获得锁后(所以才会从notFull.await()
恢复执行),队列又变成满了。 - 此
put
函数只有成功入队后,才可能从put
调用处返回。
- 你可能会产生疑问,为什么需要重新检查一遍。因为当前线程从
- 当队列未满,则入队。
超时offer
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)//如果队列是满的,且等待时间<= 0这代表不用等待,所以直接返回false
return false;
nanos = notFull.awaitNanos(nanos);
}
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
相比上一个实现,使用的是awaitNanos
。
- 从
notFull.awaitNanos(nanos)
返回有三种原因:超时前的signal、超时前的中断、超时。- 超时前的signal。只有这种情况,才可能返回一个大于0的数字。
- 超时前的中断。返回时,抛出中断异常。
- 超时(不管之后有没有中断)。只可能返回一个小于0的数字。
- 因为超时前的signal而从
notFull.awaitNanos(nanos)
返回,需要进行虚假唤醒的检查。如果此时队列还是满的,当前线程再次进入AQS的条件队列;如果此时队列确实未满,那么入队,返回true。- 如果此时队列是满的,当前线程再次进入AQS的条件队列之前,需要检查剩余时间是否大于0,如果不是大于0,说明在
awaitNanos
上话费的时间已经超过了限制,则返回false。
- 如果此时队列是满的,当前线程再次进入AQS的条件队列之前,需要检查剩余时间是否大于0,如果不是大于0,说明在
某种情景再现:
- 当前线程调用
notFull.awaitNanos(500)
,准备进行500ns的等待。 - 别的线程在剩余时间大约还有300ns的时间时,调用了
notFull.signal()
,唤醒了当前线程。 - 当前线程从
notFull.awaitNanos(500)
处返回,返回值为300。 - 循环继续,检查却发现队列已满。
if (nanos <= 0)
不满足,继续执行notFull.awaitNanos(300)
。- 当前线程继续等待300ns。
总结
入队方法 | 是否等待 | 队列满时的处理 | 返回值 | 返回值含义 |
---|---|---|---|---|
add | 一次入队尝试,从不等待 | 抛出"Queue full"异常 | true - |
入队成功 - |
offer | 一次入队尝试,从不等待 | 返回false | true false |
入队成功 入队失败 |
put | 入队尝试失败后,会等待 | 不返回,进入条件队列继续等待 | void | 只要从put 调用处返回,就代表入队成功 |
超时offer | 入队尝试失败后,会等待 | 如果没超时,则进入条件队列继续等待; 如果超时了,返回false |
true false |
规定时间内,入队成功 规定时间内,没有入队 |
出队
peek
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return itemAt(takeIndex); // null when queue is empty
} finally {
lock.unlock();
}
}
直接返回索引处元素,可能为null(队列为空),正如peek的含义,只获取不出队。
poll
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;//执行出队动作
if (++takeIndex == items.length)//右移takeIndex,按照循环数组的方式
takeIndex = 0;
count--;//大小减1
if (itrs != null)
itrs.elementDequeued();//迭代器必要操作
notFull.signal();//通知阻塞在notFull条件队列上的第一个线程
return x;
}
此函数可能返回null,当队列为空时。
take
此函数与put
相对应。实现与put
完全对称,好像没什么好讲的。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)//虚假唤醒检查
notEmpty.await();
return dequeue();//如果队列确实不空,那么执行出队动作
} finally {
lock.unlock();
}
}
超时poll
此函数与超时offer
相对应。实现与超时offer
完全对称。
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)//如果队列是空的,且等待时间<= 0这代表不用等待,所以直接返回null
return null;
nanos = notEmpty.awaitNanos(nanos);
}
return dequeue();//如果队列确实不空,那么执行出队动作
} finally {
lock.unlock();
}
}
总结
出队方法 | 是否等待 | 队列空时的处理 | 返回值 | 返回值含义 |
---|---|---|---|---|
peek | 一次获得尝试,从不等待 | 不在乎队列空,直接返回元素 | 非null null |
队列不空 队列空 |
poll | 一次出队尝试,从不等待 | 返回null | 非null null |
队列不空 队列空 |
take | 出队尝试失败后,会等待 | 不返回,进入条件队列继续等待 | void | 只要从take 调用处返回,就代表出队成功 |
超时poll | 出队尝试失败后,会等待 | 如果没超时,则进入条件队列继续等待; 如果超时了,返回false |
非null null |
规定时间内,出队成功 规定时间内,没有出队 |
remove 删除操作
该函数如果删除的不是队首元素,会涉及到整体移动的过程,可能会比较耗时,不建议使用。
现在队列中非null元素的范围是[takeIndex, putIndex)
的左闭右开的区间。
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;
do {
if (o.equals(items[i])) {
removeAt(i);
return true;
}
if (++i == items.length)
i = 0;
} while (i != putIndex);
//到达区间[takeIndex, putIndex)的边界,说明所有非null元素都找遍了
}
return false;//没有找到元素
} finally {
lock.unlock();
}
}
循环从[takeIndex, putIndex)
的左边界开始,直到右边界结束。如果找到元素,则删除它。
void removeAt(final int removeIndex) {
// assert lock.getHoldCount() == 1;
// assert items[removeIndex] != null;
// assert removeIndex >= 0 && removeIndex < items.length;
final Object[] items = this.items;
if (removeIndex == takeIndex) {
//如果刚好删除的是队首,那刚好是一个出队动作
// removing front item; just advance
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
} else {
//其他情况
//[i,putIndex)区间内的第一个元素被删除,需要往左压实这个区间
final int putIndex = this.putIndex;
for (int i = removeIndex;;) {
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;//最后putIndex当然也得左移,i此时肯定是putIndex - 1
break;
}
}
count--;
if (itrs != null)
itrs.removedAt(removeIndex);
}
notFull.signal();
}
- 如果刚好删除的队首元素,那刚好是一次出队操作。
- 如果是其他情况,现在删除的是
i
索引元素,但为了队列非null元素连续(考虑循环数组也得连续),那么[i, putIndex)
区间内的第一个元素已经被删除变成null了,需要往左压实,即[i+1, putIndex)
内的元素整体左移。
总结
- 当队列为空或为满时,
takeIndex putIndex
二者才会相同。 - 所有常用操作都需要加锁,甚至是属于读操作的
peek
,因为加锁强制内存刷新,能让线程看到最新的队列。 - 入队出队操作,都有一次尝试版本,和阻塞等待版本。
- 使用
Lock
来控制并发操作。 - 两个
Condition
的使用,是控制阻塞等待的关键。 - 删除操作支持删除内部元素。