面试必考AQS-await和signal的实现原理

Condition接口

这个接口为我们提供了2类方法,await()和signal(),其实现类ConditionObject,是AQS中的一个子类。在介绍AQS结构的文章中,ConditionObject类被跳过了,这个类的存在与CLH模型关联度不是很强,但在并发编程中却是不可或缺的一环,它提供的await()和signal()方法,能够为多线程之间交互提供帮助,能让线程暂停和恢复,是很重要的方法。

ConditionObjec类

我们先来看一下它的内部结构。

成员变量:看来ConditionObject中也维护着一个队列,我们称它为“等待队列”。

private transient Node firstWaiter; // 首节点
private transient Node lastWaiter; // 尾结点

常量:

/** Mode meaning to reinterrupt on exit from wait */
private static final int REINTERRUPT =  1; // 从等待状态切换为中断状态
/** Mode meaning to throw InterruptedException on exit from wait */
private static final int THROW_IE    = -1; // 抛出异常标识

实例方法很多,我们从最重要的开始分析,因为实现了Condition接口,因此await()和signal()就是切入点。

线程等待await()

我先整理出一个await()内部调用流程:

// 向队列中添加节点并返回
private Node addConditionWaiter() {...}
// 释放节点持有的锁
final int fullyRelease(Node node) {...}
// 判断节点是否在同步队列中
final boolean isOnSyncQueue(Node node) {...}
// 检查线程是否中断,如果是则终止Condition状态并加入到同步队列
private int checkInterruptWhileWaiting(Node node) {...}
// 操作节点去申请锁
final boolean acquireQueued(final Node node, int arg) {    ...}    
// 清理等待队列中无效节点
private void unlinkCancelledWaiters() {...}
// 处理线程中断情况
private void reportInterruptAfterWait(int interruptMode){...}

接下来挨个分析实现方法,先从入口await()方法开始。

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter(); // t1
    int savedState = fullyRelease(node); // t2
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) { // t3
        LockSupport.park(this); //t4
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) //t5
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE) // t6
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters(); // t7
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode); // t8
}

在t1位置,先看一下addConditionWaiter()方法,看名字是增加了一个条件等待对象,应该就是向等待队列中操作了。

private Node addConditionWaiter() {    
	Node t = lastWaiter; // 获取尾部指针,看来是采用尾插法    
	// If lastWaiter is cancelled, clean out.    
	if (t != null && t.waitStatus != Node.CONDITION) {        
		unlinkCancelledWaiters(); // t7'        
		t = lastWaiter;    
	}    
	Node node = new Node(Thread.currentThread(), Node.CONDITION); // 创建一个新节点    
	if (t == null) // 尾结点为空,说明队列是空的        
		firstWaiter = node; // 初始化队列    
	else        
		t.nextWaiter = node; // 尾插    
	lastWaiter = node; // 调整尾指针指向    
	return node; // 返回新增节点对象
}

t7和t7',在await()和addConditionWaiter()方法中,都调用了unlinkCancelledWaiters(),先看一下它做了什么:

private void unlinkCancelledWaiters() {
    Node t = firstWaiter; // 拿到头节点
    Node trail = null;
    while (t != null) { // 如果头节点不为空,队列不为空
        Node next = t.nextWaiter; // 遍历等待队列
        if (t.waitStatus != Node.CONDITION) { // 如果节点的状态不是CONDITION
            t.nextWaiter = null; // 将节点移除队列
            if (trail == null) // 首次遍历,进度为0
                firstWaiter = next; // 头节点指向被移除节点的下一个节点
            else
                trail.nextWaiter = next; // 进度指向下一个节点,也是将修复被移除队列节点的影响,保证队列连续
            if (next == null)
                lastWaiter = trail; // 如果next为空,说明队列遍历完成,将尾指针指向进度节点
        }
        else // 如果节点的状态是CONDITION
            trail = t; // 保存进度
        t = next;
    }
}

那么unlinkCancelledWaiters()方法就做了一件事,遍历等待队列,将非CONDITION状态到的节点移除。

重点:在等待队列中,我们发现获取节点的后继节点时,使用的是nextWaiter属性,而非next,这就是区别“等待队列”和“同步队列”的关键。

  • 在同步队列中,获取后继节点采用的是next属性。
  • 在等待队列中,获取后继节点采用的是nextWaiter属性。

在addConditionWaiter()方法的t7'位置调用的目的:调用条件是t.waitStatus != Node.CONDITION,也就是同步队列尾结点状态不对,那么这时清理一次同步队列再插入新节点很有必要。

在await()方法的t7位置调用的目的:由于t7前面还有其他逻辑未介绍,这里我们稍后继续分析。(调用条件是node.nextWaiter != null)

说回addConditionWaiter()方法,它其实和addWaiter()方法功能差不多,向队列中添加节点,这里的队列是“等待队列”。接着分析await()方法。

int savedState = fullyRelease(node); // t2

在t2位置,调用fullyRelease(node),传入新添加的node节点,并返回一个状态:

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState(); // 获取AQS中的state值
        if (release(savedState)) { /// 调用释放锁方法
            failed = false; 
            return savedState; // 如果释放成功,返回state值
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

在fullyRelease()方法中,主要是调用了release()去释放锁。这里有个前提就是线程必须先持有锁,才能调用await()方法,进而release()释放锁。

那么就引出了await()方法暂停线程,会导致锁被释放的逻辑。

release()方法的实现,前文有提到,需要回顾的请戳《面试必考AQS-排它锁的申请与释放》。我们继续分析await()方法。

while (!isOnSyncQueue(node)) { // t3
        LockSupport.park(this); //t4
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) //t5
            break;
}

在t3位置,调用了while循环,条件是!isOnSyncQueue(node),是否不在同步队列中? 如果不在,将会执行下面的内容。

final boolean isOnSyncQueue(Node node) {
    if (node.waitStatus == Node.CONDITION || node.prev == null) 
        return false; // 以节点状态作为判断条件,如果等于CONDITION(说明在等待队列中)、或者前置节点为空,是一个独立节点
    if (node.next != null) // If has successor, it must be on queue
        return true; // 如果后继节点不为空,说明它还在同步队列中。
    /*
     * node.prev can be non-null, but not yet on queue because
     * the CAS to place it on queue can fail. So we have to
     * traverse from tail to make sure it actually made it.  It
     * will always be near the tail in calls to this method, and
     * unless the CAS failed (which is unlikely), it will be
     * there, so we hardly ever traverse much.
     * 前置节点为空,并不代表节点不在队列上,因为 CAS操作有可能失败。 因此需要从尾部遍历队列来保证它不在队列上。     
     */
    return findNodeFromTail(node); // 从尾部找到node节点
}

这里要注意一点,node的next属性是AQS的同步队列范畴的属性,在ConditionObject中是没有使用next属性的。这点在分析unlinkCancelledWaiters()方法时说明过。

// 这个方法没什么好解释的
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

那么,如果while (!isOnSyncQueue(node)) 成立,就是节点node不在同步队列上,则说明node已经释放锁了,并且进入了等待队列。接下来让线程挂起、等待被唤醒就可以了。

LockSupport.park(this); //t4
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) //t5
     break;

在t5位置,执行的条件是线程被唤醒,唤醒后首先要检查的是,在这期间线程是否有被中断,保证线程安全。

private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : // t5-1
        0;
}
final boolean transferAfterCancelledWait(Node node) {
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { // 将节点状态由CONDITION调整为0
        enq(node); // 加入同步队列 
        return true; 
    }
    /*
     * If we lost out to a signal(), then we can't proceed
     * until it finishes its enq().  Cancelling during an
     * incomplete transfer is both rare and transient, so just
     * spin.
     * 如果忘记调用signal,那么就不能继续执行了,要让它回到同步队列中。
     */
    while (!isOnSyncQueue(node)) // 判断线程是否在同步队列,直到回到同步队列(取消,也要先让node回到同步队列)
        Thread.yield();  // 让出CPU时间
    return false; // 修改node状态失败,返回false
}

在t5-1位置,如果线程为中断状态,则进入transferAfterCancelledWait() ,里面会操作node状态由CONDITION回到初始状态0,此时如果操作成功,会将node重新放回同步队列。

如果CAS失败,则需要向下执行,有可能是其他操作改变了node状态,或许是取消的场景,因为这里进入的前提是线程已经被中断。

在结束了transferAfterCancelledWait()方法后,根据返回的true/false,确定 返回THROW_IE还是REINTERRUPT状态,如果没有中断则返回0,也就是interruptMode的初始值。

总结t5的逻辑,线程被唤醒后,检查线程状态,如果是中断状态,要尝试将node的节点状态变更为0,如果变更成功,则判定中断原因是异常,如果变更失败,要给线程时间让其他线程将node放回同步队列。

在t5位置,如果返回的不是初始值,则外层while会被break;如果是初始值,则会判断是否进入同步队列,是则结束循环,否则说明还在等待队列,需要继续被挂起。

当循环结束,后续流程就需要 让线程重新进入锁竞争状态,并且前面判断了那么多线程状态,也要根据返回值处理一下。

if (acquireQueued(node, savedState) && interruptMode != THROW_IE) // t6
	interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
	unlinkCancelledWaiters(); // t7
if (interruptMode != 0)
	reportInterruptAfterWait(interruptMode); // t8

在t6位置,让节点线程再次去申请锁,同时传入挂起前保存的资源值saveState,节点回到竞争状态后就是AQS申请逻辑,可以交给AQS了;对于await()来说,剩下的就是处理线程状态了。

如果interruptMode != 异常,则调整interruptMode的值为REINTERRUPT。也就是说,如果线程申请锁成功,未来会让线程中断。

在t7位置,如果节点node有后继节点,那么需要将node从等待队列移除

在t8位置,如果interruptMode的值不为0,也就是不正常状态,进入reportInterruptAfterWait()方法。

private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    if (interruptMode == THROW_IE) // 如果为异常状态
        throw new InterruptedException(); // 抛出异常
    else if (interruptMode == REINTERRUPT) // 如果为中断状态
        selfInterrupt(); // 设置线程中断
}
static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

以上就是await()方法的全部流程,大致可归纳为:

1、将持有锁的线程包装为node,并放入等待队列
2、将持有的锁释放,保持持有锁时申请的资源值
3、循环判断节点node释放在同步队列中,如果没有则挂起线程
4、线程被唤醒后,要判断线程状态
5、让线程去申请锁,根据申请规则,如果申请失败会在同步队列挂起
6、如果申请成功,要根据线程状态对线程进行合理的处理:抛异常或中断

线程唤醒signal()

先整理出一个signal()内部调用流程:

public final void signal() {...} // 唤醒线程入口
protected boolean isHeldExclusively(); // 判定当前线程是否持有锁
private void doSignal(Node first) {...} // 唤醒first节点
final boolean transferForSignal(Node node) {...} // 转换节点状态

从入口方法signnal()来分析:

public final void signal() {
    if (!isHeldExclusively()) // 抽象方法,有子类实现,用于判断当前线程是否持有锁
        throw new IllegalMonitorStateException();  // 只有持有锁的线程才能操作唤醒
    Node first = firstWaiter;  // 获取等待队列的头结点
    if (first != null) 
        doSignal(first); // 执行唤醒操作
}

入口就是一些状态判断,真正执行唤醒的是doSignal()方法:

private void doSignal(Node first) {
    do {
        // t1
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&  // t2
             (first = firstWaiter) != null); // t3
}

方法进入后,遇到一个do..while循环,先执行do内逻辑。

在t1位置,判断给定的节点first是否存在后继节点,如果不存在,将lastWaiter置为null。这里就是将等待队列清空。

接着进入t2位置,调用transferForSignal()方法:

final boolean transferForSignal(Node node) {
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) // 将节点状态恢复为0,如果修改失败返回false 
        return false; // t2-1

    /*
     * 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)) // t2-2,或者,无法调整为SIGNAL 
        LockSupport.unpark(node.thread); // 唤醒线程
    return true; 
}

在t2-1位置,将节点状态恢复为0,如果修改失败返回false。

在t2-2位置,或的判断,两个条件:

1、如果同步状态为取消,则唤醒线程,在await()逻辑中,被唤醒的线程会检查线程状态,此时的取消会导致在transferAfterCancelledWait()方法中,无法将node状态由CONDITION转为0,也就进而不停让出线程cpu时间,导致线程被取消。

2、如果compareAndSetWaitStatus(p, ws, Node.SIGNAL)==false,也就是CA无法改变ws值,就说明有其他线程在操作该node。

以上两种条件都必须要唤醒线程。

while (!transferForSignal(first) &&  // t2
             (first = firstWaiter) != null); // t3

当然以上两种条件有可能都不成立,那么就继续在t2位置执行循环,直到条件成立。

当执行了t2-1位置,也就代表节点node状态被重置,并且已经从等待队列出队,那么,在t3位置==遍历等待队列下一个节点。

在while条件中,完整逻辑是:不断尝试唤醒等待队列的头节点,直到找到一个没有被cancel的节点,跳出循环。

以上就是signal()方法的所有源码,归纳一下:

1、只有持有锁的线程才能操作唤醒
2、唤醒时要针对 等待队列 的头节点所代表的线程
3、唤醒= 线程节点node 状态重置 + node回到同步队列 + unpark线程
4、唤醒过程中如果遇到cancel状态的节点,要尝试等待队列中下一个,直到找到可被正常唤醒节点 或者 队列为空

唤醒所有线程signalAll()

在Condition接口中,还有一个signalAll()方法,目的是唤醒所有等待的节点,来分析一下源码:

public final void signalAll() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignalAll(first);
}

入口方法看了与signal()差不多,只是最后执行的方法是doSignalAll()。

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}

与doSignal()的区别是 while流程有变化,它不是找到一个可被唤醒的节点就结束,而是遍历整个等待队列,将所有节点唤醒。

尾声

Condition及ConditionObject,实现了线程的等待与唤醒行为,在并发编程中,熟练使用它们能够大大提升并行效率,减少线程空转,降低CPU消耗。

自此,AQS的主要源码已经分析完毕,后面会挑选JUC下的主要实现类做分析,来看一下之前反复提到的tryxxx()方法是如何实现以达到不同特色、不同类型的锁的。

 

推荐阅读:

面试必考AQS-AQS概览

面试必考AQS-AQS源码全局分析

面试必考AQS-排它锁的申请与释放

面试必考AQS-共享锁申请、释放及传播状态

面试必考AQS-await和signal的实现原理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章