AQS
- 前言
- 如何使用
- await
- addConditionWaiter
- fullyRelease
- isOnSyncQueue
- signal
- doSignal
- transferForSignal
- 总结
- 结束语
前言
在前一篇文章,我们分析了AQS入队的方式( addWaiter)及入队后,对队列中每个节点进行自旋+CAS获取独占锁的操作(acquireQueued)。而本文将分析Condition下的队列,又称为条件队列。
AQS中的队列分为同步队列和条件队列两种,这两种队列即相互独立又藕断丝连。 本文接下来将分析条件队列是如何新建、又是如何和同步队列藕断丝连的。
如何使用
按照"国际惯例",这里先给出一个例子:在例子中,我们通过lock.newCondition()创建了一个conditionNull 的队列,表示队列LinkedList为空的条件队列。
当我们试图调用take方法的时候,如果LinkedList为空,我们的conditionNull.await()试图将当前线程挂起。
当我们调用put方法的时候,我们又试图通过条件队列conditionNull.signal();唤醒先前调用take方法时,发现队列元素为空而被阻塞的线程。
这里可能就会有人有疑问了,我在执行put操作时,已经是占有锁了,此时我唤醒别的线程,还没释放自己占用的锁,其它线根本就不可能占据。那唤醒的线程到底何去何从呢?这里的的"去",指的就是同步队列了,"从"指的是就是我们的条件队列。
cpp
/***
*
* @Author:fsn
* @Date: 2020/4/5 16:32
* @Description
*/
public class BlockingQueue<E> {
private Logger log = LoggerFactory.getLogger(BlockingQueue.class);
private LinkedList<E> linkedList = new LinkedList<>();
private Lock lock = new ReentrantLock();
private Condition conditionNull = lock.newCondition();
public void put(E e) {
if (e == null || e.equals("")) {
throw new RuntimeException("添加的元素不能为空");
}
lock.lock();
linkedList.add(e);
conditionNull.signal();
lock.unlock();
}
public E take() {
lock.lock();
if (linkedList.isEmpty()) {
try {
log.info("线程名称 {} . 当前元素为空", Thread.currentThread().getName());
conditionNull.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
E e = linkedList.remove();
lock.unlock();
return e;
}
}
await
从上述例子中,我们可以知道,条件队列的创建的奥妙就在于await方法中。那await到底做了件什么事?如下源码所示,
cpp
public final void await() throws InterruptedException {
// 如果当前线程被中断过, 则直接抛出中断异常
if (Thread.interrupted())
throw new InterruptedException();
// 封装当前线程, 并扔到条件队列中
Node node = addConditionWaiter();
// 完全释放当前线程占用的锁, 并保存释放前(即当前)的锁状态
int savedState = fullyRelease(node);
int interruptMode = 0;
// 如果当前节点(封装好的线程)不在同步队列中
// 说明还没有被signal过
while (!isOnSyncQueue(node)) {
// 挂起
LockSupport.park(this);
// 讲道理, 上面执行后不应该执行到这里的
// 当如果线程被中断了、或者被signal了,
// 则会跑到这里
// 检查唤醒的原因, 如果是中断则跳出循环
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);
}
addConditionWaiter
上述源码中,addConditionWaiter的作用是封装当前线程, 并扔到条件队列中,它的源码如下所示, 这里我们可以回顾上一篇文章分析addWaiter的时候,它是需要依赖自旋+CAS来入队的,这里之所以不用,是因为进入await的时候就需要事先加锁了,这里可能有巨细的人说,那我偏偏不加呢?还有一个问题,为什么这里需要判断如果尾结点不为null且不为等待状态,就清除被cancel节点呢?这里可以接着往下看~
回顾一下AQS(一)中提到的waitStatus,等待状态的取值情况如下所示:
- CANCELLED :1 表明一个等待的线程被取消了
- SIGNAL : -1 表明一个等待线程的下一个线程需要被唤醒
- CONDITION : -2 当前线程正在等待中
- PROPAGATE :-3 下一次的acquire方法应该被无条件的传播
- 0:初始值
cpp
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
// 如果尾结点不为null且不为等待状态
if (t != null && t.waitStatus != Node.CONDITION) {
// 遍历链表清除被cancel的节点
unlinkCancelledWaiters();
t = lastWaiter;
}
// 包装当前线程、扔进条件队列
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
// 初始化
firstWaiter = node;
else
// 通过nextWaiter进行连接
t.nextWaiter = node;
lastWaiter = node;
// 返回包装好的节点
return node;
}
fullyRelease
进入条件队列后,执行这行代码 int savedState = fullyRelease(node);我们需要释放当前线程的锁,注意这里是完全释放的意思,对于重入锁来说,无论你重入几次,在这里都要全部释放,并且 释放前需要保存状态,以便恢复的时候使用。
cpp
final int fullyRelease(Node node) {
boolean failed = true;
try {
// 释放之前保存状态
int savedState = getState();
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
我们可以看看release的逻辑,这里又会调用tryRelease方法,而AQS中是没有对这方法进行实现,以重入锁为例,它重写了这个方法:
cpp
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
OK,看到这句代码,我觉得我们可以回答上面提到的第一个问题了, Thread.currentThread() != getExclusiveOwnerThread())即addConditionWaiter没有通过CAS,也能让程序正常运行,如果你不在外面加锁,在await方法中,会释放线程占有的锁,而真正执行释放操作的代码逻辑中会判断线程是否为独占的线程,否则就抛出异常IllegalMonitorStateException。
而fullyRelease代码模块中,执行finally 代码块时, node.waitStatus = Node.CANCELLED这句代码将被执行,也就是它会将当前线程的等待状态设置为取消,而AQS中并没有立刻清除这些状态为取消的节点,而是等到下次调用addConditionWaiter方法时,判断尾部节点是否为等待状态,不是的话就遍历链表清除被cancel的节点。
cpp
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
isOnSyncQueue
接下来就是下面这段代码了,interruptMode 可以先别管,我们先分析isOnSyncQueue方法,它会判断当前线程是否在同步队列。
cpp
int interruptMode = 0;
// 如果当前节点(封装好的线程)不再同步队列中
// 说明还没有被signal过, signal的线程会移动到同步队列中
while (!isOnSyncQueue(node)) {
// 挂起
LockSupport.park(this);
// 讲道理, 上面执行后不应该执行到这里的
// 当如果线程被中断了、或者被signal了,
// 则会跑到这里
// 检查唤醒的原因, 如果是中断则跳出循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
isOnSyncQueue源码比较简单,关键的遍历逻辑放在findNodeFromTail了方法了.
😉 findNodeFromTail 😃 重点来了,在findNodeFromTail代码中,我们的等待队列和同步队列第一次出现出现联系了。
cpp
final boolean isOnSyncQueue(Node node) {
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
if (node.next != null) // If has successor, it must be on queue
return true;
return findNodeFromTail(node);
}
/**
* Returns true if node is on sync queue by searching backwards from tail.
* Called only when needed by isOnSyncQueue.
* @return true if present
*/
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
findNodeFromTail代码中通过一个死循环,从尾部向前遍历同步队列,寻找节点。这个同步队列就是我们在分析addWaiter的时候已经讲过,它会通过自旋的方式确保每个线程都能入队。那问题来了,我们的节点什么时候会进入到同步队列中呢?
signal
为了回答上个问题,我们可以先来看看signal方法,它的源码如下所示:它先判断当前线程是否具有锁即独占线程,然后将firstWaiter赋给first,其中firstWaiter 就是我们 addConditionWaiter方法创建条件队列的时候,初始化节点后所赋值的节点。
cpp
public final void signal() {
// 是否为独占线程
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
// 唤醒第一个节点
doSignal(first);
}
doSignal
唤醒之前,我们需要重置一下firstWaiter 节点,具体的唤醒操作放在了transferForSignal。
cpp
/**
* Removes and transfers nodes until hit non-cancelled one or
* null. Split out from signal in part to encourage compilers
* to inline the case of no waiters.
* @param first (non-null) the first node on condition queue
*/
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
transferForSignal
终于到了具体的唤醒步骤了,在这个步骤,我们企图通过compareAndSetWaitStatus方法,将节点对应的线程从等到状态恢复到初始状态0(该状态处于唤醒线程、但还没有参与到锁竞争的状态中),如果设置为初始状态不成功,说明该线程被取消(中断)了。
否则,我们将进入enq队列,关于enq队列,我们已经在上篇文章中陈述过,它通过自旋+CAS的方式确保进入同步队列能够成功。
进入同步队列不就意味着你能立刻得到锁,所以有了接下来的判断,这里单独拎出来,根据前面对等待状态的描述,大于0的状态只有cancel一种,而compareAndSetWaitStatus则是试图将唤醒的线程的状态通过CAS转为SIGNAL,该状态表示的意思一个等待线程的下一个线程需要被唤醒。
而关于这句代码 Node p = enq(node) 我们要注意的是,它返回的是当前节点的前驱节点。这也就解释了接下来的操作。
cpp
if (ws > 0 ||
!compareAndSetWaitStatus(p, ws, Node.SIGNAL))
cpp
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;
}
这句代码中,我们通过判断前驱节点等待状态是否大于0(取消)或者尝试将前驱节点设置为SIGNAL状态(同步队列中的节点靠前驱节点去唤醒),当ws大于0或者设置SIGNAL不成功,可以将前驱视为被取消了,此时我们可以LockSupport唤醒当前线程了。
cpp
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
说到这里,我们可以回答这个问题了 我们的节点什么时候会进入到同步队列中呢?当某个ConditionObject对象调用signal()方法的时候(signalAll差不多),条件队列中的节点会进入到同步队列中,但真正执行进入的操作还是在enq方法。
总结
本文分析了同步队列的节点如何创建条件队列的,又是如何唤醒等待队列,而被唤醒的等待队列节点又是如何跑到同步队列的。
同步队列即CLH队列、双向队列,根据FIFO的原则,结合上一篇文章分析的addWaiter方法,我们可以知道头结点总是虚拟节点,头结点之后的节点会先获取独占锁,获取之后,如果此时来一个ConditionObject对象用了 await方法。那么会在对应的条件队列后边插入一个节点、并释放当前的独占锁。
如果获取独占锁的线程里,一个ConditionObject对象用了 signal方法,那么此时会将对应的条件队列的对头移至同步队列的队尾处。
结束语
似乎还有什么没有讲到的样子。。如果你已察觉到,恭喜盲生发现了华点了!就是下面这坨还没有说到,欢迎收看Java之AQS(三)
cpp
// 讲道理, 上面执行后不应该执行到这里的
// 当如果线程被中断了、或者被signal了,
// 则会跑到这里
// 检查唤醒的原因, 如果是中断则跳出循环
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);