JAVA阻塞队列
在学习线程池框架ThreadPoolExecutor时发现线程池的实现依赖到了阻塞队列BlockingQueue,在队列为空时take方法会阻塞当前线程,因此这里以ThreadPoolExecutor的阻塞队列默认实现LinkedBlockingQueue为入手点进行源码分析
1.BlockingQueue的成员变量
/**
* Linked list node class
*/
static class Node<E> {
E item;
/**
* One of:
* - the real successor Node
* - this Node, meaning the successor is head.next
* - null, meaning there is no successor (this is the last node)
*/
Node<E> next;
Node(E x) { item = x; }
}
/** The capacity bound, or Integer.MAX_VALUE if none */
private final int capacity;
/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();
/**
* Head of linked list.
* Invariant: head.item == null
*/
transient Node<E> head;
/**
* Tail of linked list.
* Invariant: last.next == null
*/
private transient Node<E> last;
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
BlockingQueue也有一个链表对象Node,但是这里的Node和AQS里的链表不同,AQS的链表是用来用来存放等待线程,BlockingQueue是一个集合容器因此这里是用来存放数据。
count是一个线程安全的容器计数对象,BlockingQueue也是一个线程安全类
ReetrantLock&Condition这里有两组JAVA的显示锁和对应的条件队列,这也是BlockingQueue实现take阻塞的实现关键类,后面源码也可以确认BlockingQueue使用了生产-消费者模型(Synchronized和Object.wait方法同样也可以用来实现生产-消费者模型)
2.条件队列Condition挂起
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
LinkedBlockingQueue的构造方法比较简单,这里直接看阻塞的take方法
方法会获取队列的takeLock给take方法加锁,这里用的中断响应的加锁方式。然后自旋查询队列数据数量,队列为空时会使用阻塞队列阻塞当前线程。这里重点看一下为什么notEmpty.await方法会阻塞线程
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
这个方法首先在阻塞队列最后增加一个CONDITION节点,并释放当前线程拥有的lock,因此如果在await之前没有获取到Condition对应的Lock会抛出异常,这点和Object.wait方法一致。
释放掉lock之后开始自旋,如果阻塞队列只有一个Condition节点使用LockSupport.park挂起当前线程释放CPU资源。这里阻塞之后再次启动就需要调用Condition.signal方法,这个方法在后面篇幅再进行讨论,这里继续跟进await方法。
线程唤起之后会调用acquireQueued方法获取独占锁,上一篇文章分析过这个方法,这里就不多说了,后续就是处理一下线程取消和中断的逻辑。
2.条件队列Condition唤醒
使用Condition.await的线程是需要唤醒才会继续执行下一行代码,这里看一下唤醒方法signal。
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
Condition中维护了一个线程的等待列表,signal只唤起一个线程就只拿到链表的头节点,这个列表中的阻塞线程节点是在wait方法中追加进链表的。doSignal的方法将阻塞队列的头结点取出,将第二个节点设置为头结点,同时需要将该点加入AQS的等待队列(自旋添加)放在预备节点(waitStatus值为0)之后,后续调用LockSupport.unpark方法让之前wait挂起的线程重新执行(这里最后的if判断看了半天没明白怎么进去的,希望有评论大佬指点),线程重新执行后会去重新获取锁,获取到锁的线程才会继续执行下面的逻辑。