阻塞队列ArrayBlockingQueue源码解读
目录
前言
BlockingQueue,是java.util.concurrent 包提供的用于解决并发生产者 - 消费者问题的最有用的类,它的特性是在任意时刻只有一个线程可以进行take或者put操作,并且BlockingQueue提供了超时return null的机制,在许多生产场景里如线程池都可以看到这个工具的身影。
常见的4种阻塞队列:
- ArrayBlockingQueue 由数组支持的有界队列
- LinkedBlockingQueue 由链表支持的可选有界队列
- PriorityBlockingQueue 由优先级堆支持的无界优先级队列
- DelayQueue 由优先级堆支持的、基于时间的调度队列
下面以ArrayBlockingQueue为例,介绍其工作原理。
源码解读
ArrayBlockingQueue通过Reentantlock实现同步机制,并通过Condition实现条件等待。首先看一下ArrayBlockingQueue的属性变量:
/** 内部维护的队列节点数组 */
final Object[] items;
/** 下一个take操作的数组下标 */
int takeIndex;
/** 下一个put操作的数组下标 */
int putIndex;
/** 队列中的节点个数 */
int count;
/** 队列同步锁 */
final ReentrantLock lock;
/** take操作等待条件 */
private final Condition notEmpty;
/** put操作等待条件 */
private final Condition notFull;
进入put操作源码:
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
/**往队列放元素前先加锁,这里用了lockInterruptibly,表示可响应中断*/
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await(); //如果队列满了,则在notFull这个条件上等待,即进入条件队列
enqueue(e); //被唤醒后,继续put操作,入队
} finally {
lock.unlock();
}
}
下面进入notFull.await()-->AbstractQueuedSynchronizer.await():
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter(); //创建一个node放入条件等待队列
int savedState = fullyRelease(node); //释放state资源
int interruptMode = 0;
/**
*这里判断是否在同步队列中,如果不在同步队列中会无限被阻塞,当然这里node刚被创建进入
*条件队列,肯定不在,进入循环,线程被挂起,等待被唤醒
*/
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
/**
*这里是不是很熟悉acquireQueued()这个方法也被Reentlock复用了,用于同步队列中的节
*点尝试获取锁
*/
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters(); //剔除无效节点
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode); //这里interruptMode用于接收该节点是否是由于signal或是中断唤醒的,如果是中断唤醒的,会做相应处理
}
说一下acquireQueued(node, savedState)这个方法,在之前的Reentlock源码中有详细介绍(传送门:Reentantlock源码解读_w7sss的博客-CSDN博客),这里就略过了。下面看AbstractQueuedSynchronizer.addConditionWaiter()入条件队列的方法:
private Node addConditionWaiter() {
Node t = lastWaiter;
//如果队尾节点为null或状态不为Node.CONDITION,则为无效节点,直接干掉
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION); //构建条件队列节点
if (t == null)
firstWaiter = node; //如果lastWaiter还为null,说明队列为空,直接设为头节点
else
t.nextWaiter = node; //入队
lastWaiter = node;
return node;
}
入队成功后,开始释放state资源,AbstractQueuedSynchronizer.fullyRelease():
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
if (release(savedState)) { //这里release释放资源,同Reentlock
failed = false;
return savedState; //返回资源状态
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
释放完state资源后,就会来到await()方法的这个循环,上面提到:
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
线程在这里被阻塞,直到在condition上被唤醒,node会从条件队列移到同步队列,开始排队获取锁,这里就和Reentantlock中的逻辑一样了,如果拿到了锁,就会回到上面的第一个方法put操作中的enqueue将元素放入阻塞队列:
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 = 0;
count++;
notEmpty.signal(); //唤醒消费者线程
}
入队之后,在最后唤醒消费者线程。进入notEmpty.signal()--->AbstractQueuedSynchronizer.doSignal():
private void doSignal(Node first) { //first为条件队列的头节点
do {
//do循环体:如果条件队列中只有firstWaiter一个节点了,则完全清空条件队列
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) && (first = firstWaiter) != null);
}
其中transferForSignal(first)把条件队列的头节点移到同步队列中,如果失败则会循环在下一个节点上重试:
final boolean transferForSignal(Node node) {
/*
* cas修改node的状态为0
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* enq执行入同步等待队列操作,返回前驱节点p,如果p的状态>0或者修改p的状态为Node.SIGNAL
* 失败则直接唤醒node节点。
* 这里要注意的是,同步队列中的节点能否被唤醒取决于其前驱节点的状态(需要为Node.SIGNAL)
* 如果成功把前驱节点的状态设置为Node.SIGNAL,那node节点就可以放心地去同步队列中排队了,否则因为将来无法被唤醒,所以只好提前唤醒node
*/
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
至此,put源码结束了,至于take操作,为消费者线程,其原理和生产者基本一样,只是入队换成了出队,就不重复讲解了。
这里我们可以看到,条件队列并不直接参与竞争锁的操作,而是要先转换成同步队列中的节点,才有资格去竞争锁。