Java多线程总结(三):[AQS]3. 条件队列相关代码解析

Java多线程总结(三):[AQS]3. 条件队列相关代码解析_第1张图片

回到MESA管程,我们已知在MESA中发生条件变量等待时,需要:

  1. 进入条件等待队列
  2. 释放入口锁

当条件满足时,需要:

  1. 回到入口重新尝试获取入口锁,或进入入口等待队列
  2. 回到当初的上下文,再次检测条件变量是否满足

又已知Java的锁机制是参考MESA,所以我们推测J.U.C.中相关逻辑框架应与上面描述的基本一致

等待

关于await方法值得注意的事情:

  • 处于等待状态的节点,可能由signal唤醒,也可能由线程中断唤醒
    • 节点被唤醒后会从条件队列转移到CLH队列,该转移动作可能会由不同线程执行
      • 中断发生导致节点唤醒后,await节点线程会尝试队列转移
      • 上面二者都是通过CAS修改节点状态的方式抢夺转移权
      • signal的节点也会尝试转移
  • await方法里判断节点进入CLH队列后,会承担起获取CLH锁的责任,也就是说await方法其实还包含了acquire方法

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    //第一部分:组装一个condition状态的node,放入条件等待队列。注意这个node对象和之后重新进入CLH队列的node对象是同一个
    Node node = addConditionWaiter();
    //第二部分:释放资源
    int savedState = fullyRelease(node);//IllegalMonitorStateException在这里
    int interruptMode = 0;
    
    //被signal之后会被转移到入口的同步队列,所以当前节点如果没有出现在入口同步队列中,就认为还处于等待状态,自旋阻塞
    while (!isOnSyncQueue(node)) {
        //第三部分:阻塞等待
        LockSupport.park(this);
        //被unpark或则线程中断都会走到该逻辑,下面这段得到三个状态:
        // 0-没有发生中断,正常signal, 在signal的逻辑中进行节点转移(到入口队列)
        // -1 THROW_IE :  发生了线程中断走到当前流程,并且由当前流程完成了对节点的转移(没有被signal就进入等待队列了)
        // 1 REINTERRUPT : 发生了线程中断走到当前流程,但是同时也发生了signal,并且signal的转移流程抢到执行权
        // 注意checkInterruptWhileWaiting中会重置线程中断状态
        //第四部分:队里转移(视情况可能无需由当前线程转移)
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    
    //重新走获取入口锁的流程,但是node是复用的之前等待队列里的
    //如果等待过程中发生了中断,则两种处理方式:1.抛异常; 2.修改状态位
    //这里的逻辑是:将node的转移过程中的中断和获取资源过程中的看做一共2次
    /**
     * 重新走获取入口锁的流程,但是node是复用的之前等待队列里的
     * 如果等待过程中发生了中断,则两种处理方式:1.抛异常; 2.修改状态位
     * 这里的逻辑是:将“中断导致转移”和“中断导致获取到资源”看做2次中断事件,根据事件次数:
     *  0次,不处理
     *  1次,修改状态位
     *  2次,抛异常
     */
    //第五部分:后续逻辑执行
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}


private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
            (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
            0;
}

唤醒

唤醒的逻辑非常简单,值得一提的事情:

  1. 有IllegalMonitorStateException的异常检测,遵循MESA管程的规范
  2. 所谓唤醒就是将节点从等待队列转移到CLH队列,signal和signalAll的区别就是转移一个还是全部
public final void signal() {
    //这个异常符合MESA管程模型,condition的释放和获取,需要在入口锁的作用范围内
    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);
}

//doSignalAll基本也是一样的逻辑
private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}

final boolean transferForSignal(Node node) {
    //这段CAS是抢夺转移权。在await方法中也会涉及到队列转移的操作
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    //enq返回的是node入队后的pre
    Node p = enq(node);
    int ws = p.waitStatus;
    //把pre的status设置成signal,确保node有机会得到唤醒
    //如果设置失败了,唤醒它,靠它自己了
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

你可能感兴趣的:(Java,java)