大厂面试必会AQS(3)——从Condition源码认识AQS

一 引读

对AQS不够了解的同学,请务必先观看博客
《大厂面试必会AQS(1)——从ReentrantLock源码认识AQS》

需要首先了解的知识:

1.Condition

Condition内部主要是由一个装载线程节点 Node 的 Condition Queue 实现
对 Condition 的方法(await, signal等) 的调用必需是在本线程获取了独占锁的前提下 所以 Condition Queue 内部是一条不支持并发安全的单向 queue (这是相对于 AQS 里面的 Sync Queue)而Sync Queue同步对类是支持并发的双向链表

2.ConditionObject

ConditionObject是AQS的一个内部类,实现Condition接口,类中的两个重要成员变量firstWaiter和lastWaiter(条件队列的首个、最后一个Node节点)

/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
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()为本文重点

3.与同步队列的联系

(1)同步队列依赖一个双向链表来完成同步状态的管理,当前线程获取同步状态失败 后,同步器会将线程构建成一个节点,并将其加入同步队列中;而条件队列是单向链表
(2)通过signal或signalAll将条件队列中的节点转移到同步队列
(3)调用await方法进行阻塞(从同步队列转化到条件队列
(4)同步队列拥有的prev前驱节点和next后继节点,条件队列只有nextWaiter即下一个等待节点。
(5)一个线程只能存在于两个队列中的一个

二 await源码分析

await方法使当前线程阻塞等待,直到被通知或者被中断。
与该condition关联的lock会被自动释放,并且由于线程调度的原因线程变得不可用,其他线程调用了这个condition的signal()方法或者signalAll()方法,或者是其他线程调用当前线程的Thread.interrupt()方法;

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);
        //检查是否被中断过,没有中断则为0
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
        //如果被中断过跳出当前循环
            break;
    }
    //获取独占锁同时传入挂起前保存的资源值saveState
    //如果interruptMode != 异常,则调整interruptMode的值为REINTERRUPT,接下来会让线程线程中断
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
    //从条件队列中解除cencelled节点。
        unlinkCancelledWaiters();
    if (interruptMode != 0)//如果线程中断过
    //抛异常或者中断线程
        reportInterruptAfterWait(interruptMode);
}

await方法大致分为
1.前置检查:如果线程被中断,抛出中断异常

if (Thread.interrupted())
    throw new InterruptedException();

2.将持有锁的线程包装为node,放入等待队列,删除等待队列中非CONDITION节点

Node node = addConditionWaiter();
/**
 * Adds a new waiter to wait queue.
 * @return its new wait node
 */
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();//清除cancelled的节点
        t = lastWaiter;
    }
    //包装节点
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)// 尾结点为空,队列为空,初始化队列
        firstWaiter = node;
    else
        t.nextWaiter = node;//从尾结点添加
    lastWaiter = node;
    return node;
}

addConditionWaiter方法调用了unlinkCancelledWaiters来清除cancelled的节点

private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;//辅助指针,上一个有效节点
    while (t != null) {
        Node next = t.nextWaiter;//next指针
        if (t.waitStatus != Node.CONDITION) {
        //如果当前节点状态不是CONDITION,删除当前节点
            t.nextWaiter = null;
            if (trail == null)//因为是指向上一下有效节点,如果为null,首节点指向被移除节点的下一个节点
                firstWaiter = next;
            else
            //否则彻底删当前节点t
                trail.nextWaiter = next;
            if (next == null) // 如果next为空,说明队列遍历完成,将尾指针指向辅助节点
                lastWaiter = trail;
        }
        //如果当前节点状态是CONDITION,即有效节点
        else
        //辅助指针,指向当前有效节点
            trail = t;
            //t指针后移,开始遍历下一个节点。如果next == null,则遍历结束
        t = next;
    }
}

unlinkCancelledWaiters就是遍历等待队列,将非CONDITION状态到的节点移除。

3. 释放节点持有的锁,并唤醒后继节点,返回持有的锁资源

int savedState = fullyRelease(node);
/**
 * Invokes release with current state value; returns saved state.
 * Cancels node and throws exception on failure.
 * @param node the condition node for this wait
 * @return previous sync state
 */
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;
    }
}

fullyRelease方法会释放当前锁资源,并返回释放的savedState 在被唤醒或者被中断后重新竞争锁时作为入参,保证重入锁的安全性,其中的release方法《大厂面试必会AQS(1)——从ReentrantLock源码认识AQS》已经讲过,不再赘述。

4.如果节点不在同步队列中,即还在条件队列中,阻塞当前线程直至被中断唤醒或者被其他线程唤醒

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

首先看isOnSyncQueue(node)方法

final boolean isOnSyncQueue(Node node) {
//节点状态为CONDITION(说明在等待队列中)、或者前置节点为空,是一个独立节点,证明不再同步队列,则处于等待队列
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    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);
}

看一下findNodeFromTail方法是从队列尾部往前找,找到返回true,//找到最后都没有,返回false

/**
 * 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)//找到返回true
            return true;
        if (t == null)//找到最后都没有,返回false
            return false;
        t = t.prev;
    }
}

如果isOnSyncQueue返回false,调用LockSupport.park(this)将当前线程挂起直至被中断唤醒或者被其他线程唤醒,退出while循环的关键点在于
1:checkInterruptWhileWaiting方法。被中断返回1或者-1break出whhile循环
2:isOnSyncQueue返回true,当前condition被调用signal或signalAll方法,唤醒线程并且加入到同步队列

下面分析checkInterruptWhileWaiting方法

/**
 * Checks for interrupt, returning THROW_IE if interrupted
 * before signalled, REINTERRUPT if after signalled, or
 * 0 if not interrupted.
 */
private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?//被中断,调用transferAfterCancelledWait方法
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}
final boolean transferAfterCancelledWait(Node node) {
//CAS跟新Node的状态从CONDITION -2到0
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
    //CAS成功,当前节点插入同步队列
        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))
        Thread.yield();// 让出CPU
    return false;
}

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

5.当循环结束,后续流程就需要 让线程重新进入锁竞争状态并根据中断状态对线程进行合理的处理

if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
    interruptMode = REINTERRUPT;

acquireQueued方法不再赘述,会循环竞争锁资源并唤醒后继节点
如果竞争的时候发生了中断则返回true,接下来如果interruptMode != 异常,则调整interruptMode的值为REINTERRUPT。接下来如果节点nextWaiter 不为null,清空等待队列的cancelled节点

if (node.nextWaiter != null) // clean up if cancelled
    unlinkCancelledWaiters();
if (interruptMode != 0)//如果被中断
//中断或者抛异常
    reportInterruptAfterWait(interruptMode);

如果中断过,调用reportInterruptAfterWait方法根据interruptMode抛出异常或者再次中断

/**
 * Throws InterruptedException, reinterrupts current thread, or
 * does nothing, depending on mode.
 */
private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}

总结:
1、前置检查:如果线程被中断,抛出中断异常
2、将持有锁的线程包装为node,放入等待队列,删除等待队列中非CONDITION节点
3、释放节点持有的锁,并唤醒后继节点,返回持有的锁资源
4、如果节点不在同步队列中,即还在条件队列中,阻塞当前线程直至被中断唤醒或者被其他线程调condition.signal/condition.signalAll方法唤醒
5、当线程被唤醒,后续流程就需要 让线程重新进入锁竞争状态
6、如果申请成功,要根据中断状态对线程进行合理的处理:抛异常或中断

二 线程唤醒signal()源码分析

唤醒等待线程。
如果存在线程在条件上等待,选择其中一个作为被唤醒的线程。
在调用该方法时,要求当前线程持有与condition关联的Lock。否则抛出IllegalMonitorStateException。

public final void signal() {
    if (!isHeldExclusively())//当前线程不持有锁抛异常
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}~

可以看到如果当前线程不持有锁抛异常,然后取等待队列的第一个节点来唤醒doSignal(first)

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
        //没有后续节点,清空队列
            lastWaiter = null;
            //将头结点从等待队列中移除
        first.nextWaiter = null;
       // while中transferForSignal方法对头结点做真正的处理~
    } while (!transferForSignal(first) &&
    //下一个节点
             (first = firstWaiter) != null);
}~

如果没有后续节点,清空队列,将头结点从等待队列中移除,在while中循环唤transferForSignal(first)直至成功唤醒一个节点

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;//获得前驱节点状态
    //如果节点取消或者不能变更为SIGNAL状态,需要唤醒线程
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}~

这里有个疑问:signal方法,为什么不像Object.notify方法那样,最终总会调用unpark方法唤醒线程,而是限制了条件呢

从前面的知识中可一知道,同步队列的头结点释放锁会唤醒后继节点的线程,有个前提条件是,节点的前驱节点的waitStatus必须为SIGNAL,才会在释放锁时唤醒后继节点。所以,如果前驱节点的waitStatus为cancelled,或者将其waitStatus设置为SIGNAL的cas操作执行不成功时,就需要在这里调用unpark方法唤醒线程。

最后,如果对于为什么中断可以唤醒线程有兴趣的同学这里推荐两篇博客:
《理解java的interrupt机制》通过demo理解中断
《jvm源码分析之interrupt()》从jvm源码理解中断

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值