目录
注意:本文转自 https://blog.csdn.net/anlian523/article/details/107577452
前言
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。
某种情景再现:
- 当前线程调用notFull.awaitNanos(500),准备进行500ns的等待。
- 别的线程在剩余时间大约还有300ns的时间时,调用了notFull.signal(),唤醒了当前线程。
- 当前线程从notFull.awaitNanos(500)处返回,返回值为300。
- 循环继续,检查却发现队列已满。
- if (nanos <= 0)不满足,继续执行notFull.awaitNanos(300)。
- 当前线程继续等待300ns。
总结
出队
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();
}
}
总结
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的使用,是控制阻塞等待的关键。
- 删除操作支持删除内部元素。