signal方法
signal方法只会唤醒condition链表中一个线程,从链表的头节点开始。如果节点唤醒失败,尝试唤醒下一个节点线程,直到成功唤醒某一线程。其执行流程:
- 判断当前线程是否持有锁资源,如果没有抛出异常
- 从单向链表的头节点开始遍历,找到第一个可以将状态从-2改成0的节点
- 将节点脱离Condition单向链表
- 将Node状态从-2改成0
- 将Node节点添加到AQS双向链表中
- 判断添加到AQS双向链表中的节点的前一个节点的waitStatus是否大于0,如果是,说明此节点是无效节点。当前节点排在此节点后面,永远不会被唤醒。所以在这种情况下,需要唤醒当前节点的线程。因为在await方法中,线程从挂起处被唤醒后,会去执行acquireQueued方法,尝试获取锁资源,如果获取失败,清除AQS队列中的无效节点,将当前节点排到有效节点后面。
- 除了上述的情况外,还有一种情况会使得当前节点添加到AQS队列后无法被唤醒,就是当前节点刚被添加到AQS队列后,其前一个节点就被标识为无效节点。为了避免这种情况,需要在判断前一个节点是有效节点后,再判断通过CAS将前一个节点状态改成-1是否成功,如果不成功,说明前一个节点的状态发生了改变,此时也需要唤醒当前节点的线程。
- 如果当前节点添加到AQS后,前一个节点的状态小于等于0,并且可以将状态改成-1,那么就不需要在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) {
// 将Node的状态从-2改成0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 将Node添加到AQS队列中,并且返回前一个节点
Node p = enq(node);
int ws = p.waitStatus;
// 如果前一个节点的状态为1,表示是个要取消的节点,则立马唤醒当前节点,基于acquireQueued方法,
// 当前节点会找一个正常的prev节点,并且挂起当前节点
// 如果prev节点正常,但是CAS修改prev节点状态失败了。证明prev节点因为并发原因导致状态改变。
// 为了避免当前节点无法被正常唤醒,还是要立即唤醒当前节点,让其找到一个正常的prev节点
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}