【JUC 并发编程】— ConditionObject 源码解析

Condition 接口

线程基础一文中说道,Java 中任何一个对象都拥有一组监视器方法(定义在 java.lang.Object 上),主要包括 wait()wait(long timeout)notify() 以及 notifyAll() 方法。而且这些方法使用时有个前提,就是必须先获得对象的监视器,也就是调用代码必须包含在 synchronized 语句中。而 Condition 是与 Lock 接口配合使用,使用 Condition 接口方法前必须先获得锁。

Condition 接口主要方法如下

public final void await() throws InterruptedException
public final void awaitUninterruptibly()
public final long awaitNanos(long nanosTimeout) throws InterruptedException
public final boolean awaitUntil(Date deadline) throws InterruptedException

public final void signal()
public final void signalAll()

Condition 使用示例代码如下

Lock lock = new ReentranLock();
Condition condition = lock.newCondition();

lock.lock();
try {
    condition.await();
} finally {
    lock.unlock();
}

ConditionObject

Condition 对象是由 Lock 对象创建

Condition newCondition();

Conditon 只是个接口,主要实现类为 AbstractQueuedSynchronizer 中的内部类 ConditionObject

public class ConditionObject implements Condition, java.io.Serializable {
    /** First node of condition queue. */
    private transient Node firstWaiter;
    /** Last node of condition queue. */
    private transient Node lastWaiter;
}

每个 Condition 对象都包含着一个 FIFO 等待队列,ConditionObject 类中 firstWaiterlastWaiter分别指向等待队列的首节点、尾节点。节点之间通过 nextWaiter 连接起来。等待队列的结构如下图所示
image
Object 监视器模型上,一个对象拥有一个同步队列和一个等待队列,而同步器拥有一个同步队列和多个等待队列,对应关系如下图!
image

主要方法

await

await() 方法代码如下

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);
        // 当前线程从 LockSupport.park(this) 返回
        // 1.signal 返回 2.被中断返回
        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);
}

await()方法的大体逻辑:添加节点到等待队列的尾部,释放同步状态(锁),然后阻塞当前线程。当线程被唤醒后,重新获取同步状态(锁)。只不过在这个过程中需要考虑线程被中断的情况,所以代码中有不少地方是处理中断的逻辑。

梳理完主要流程,再来看一些具体方法:

addConditionWaiter() 添加节点到等待队列尾部

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // 如果尾节点已经被取消,则从等待队列中移除
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // 以当前线程构建 Node
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        // 当前尾节点的下一个节点指向新构建的节点
        t.nextWaiter = node;
    // 指定当前节点为尾节点
    lastWaiter = node;
    return node;
}

代码逻辑很简单,需要注意的是代码中并没有使用 CAS 的方式来添加节点,这是因为执行 await() 方法的前提是当前线程获取到锁,后面的操作自然就是线程安全的。

fullyRelease() 方法在【Java 并发编程】——AQS 源码探索之独占式一文中已作分析,这里就不在赘述。

isOnSyncQueue() 顾名思义,判断节点是否在同步队列中

final boolean isOnSyncQueue(Node node) {
    // 节点状态为 Node.CONDITION
    // 节点的 prev 节点为空(同步队列中节点的 prev 节点不会为空)
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    
    // 节点的 next 节点不为空(next 属性只会存在于同步队列,等待队列只有 nextWaiter)
    if (node.next != null) // If has successor, it must be on queue
        return true;
        
    // 上面条件都不满足,则从尾部遍历同步队列,查找指定节点
    return findNodeFromTail(node);
}

代码首次执行到 isOnSyncQueue() 时,当前线程构成的节点已从同步队列“转移”到等待队列,代码执行进入到 while 循环里面,当前线程被阻塞。那什么时候被唤醒呢?

  1. 别的线程调用 signal() 唤醒当前线程
  2. 当前线程被中断

通过 signal() 唤醒

signal() 唤醒的过程中,会把当前节点从等待队列“转移”到等待队列,isOnSyncQueue() 方法会返回 false,循环结束

通过中断唤醒

当前线程被中断,执行 if 里面逻辑

if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
    break;
    
/**
 * Checks for interrupt, returning THROW_IE if interrupted
 * before signalled, REINTERRUPT if after signalled, or
 * 0 if not interrupted.
 *
 * 检查中断
 * 如果在被 signal 之前被中断,则返回 THROW_IE(直接抛异常)
 * 如果在被 signal 之后被中断,则返回 REINTERRUPT(再次中断)
 * 否则返回 0,表示没有被中断
 */
private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}

/**
 * Transfers node, if necessary, to sync queue after a cancelled
 * wait. Returns true if thread was cancelled before being
 * signalled.
 * 
 * 等待队列节点被取消(中断)后,尝试“转移”到同步队列
 */
final boolean transferAfterCancelledWait(Node node) {
    // 修改节点状态为初始状态
    // 修改成功,说明节点还没被 signal(signal 前中断)
    // 因为 signal 也会把节点状态修改为初始状态,如果已经被 signal,这里 cas 会失败
    if (compareAndSetWaitStatus(node, Node.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(signal 后中断)
     * signal() 方法会把当前节点从等待队列“转移”到同步队列,即执行 enq() 方法,
     * 在完成“转移”之前,这里代码不能继续往下执行,所以做自旋操作就够了
     * 当节点成功“转移”到同步队列,isOnSyncQueue(node) 返回 true,则退出 while 循环
     *
     */
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

中断唤醒为什么要区分是在 signal 前后,因为这两者处理方式是不同的。如果当前线程在被 signal 之前就被中断了,也就是在阻塞的过程中被中断,还没到应该唤醒的时候,那就应该抛出异常(当然这里是响应中断异常)。如果是在 signal 之后被中断,此时节点处于同步队列中,后续该如何处理该异常,交给同步队列去处理就好。所以当检测到当前线程被中断,我们只用再次中断当前线程即可。具体代码如下

// 不等于0,表示线程被中断
if (interruptMode != 0)
    reportInterruptAfterWait(interruptMode);
    
private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}

awaitUninterruptibly

不响应中断等待

public final void awaitUninterruptibly() {
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    boolean interrupted = false;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if (Thread.interrupted())
            interrupted = true;
    }
    if (acquireQueued(node, savedState) || interrupted)
        selfInterrupt();
}

当检测到异常后,重新中断即可。

signal

唤醒操作

/**
 * Moves the longest-waiting thread, if one exists, from the
 * wait queue for this condition to the wait queue for the
 * owning lock.
 *
 * 把等待队列中等待时间最长的节点(firstWaiter)移动到同步队列中去
 */
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);
}

/**
 * Transfers a node from a condition queue onto sync queue.
 * Returns true if successful.
 * @return true if successfully transferred (else the node was
 * cancelled before signal).
 *
 * 把节点从等待队列中转移到同步队列,转移成功返回 true
 * 失败则说明节点在 signal 前已经被取消
 */
final boolean transferForSignal(Node node) {
    // 更改状态失败,说明节点已经被取消
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    
    // 加到同步队列,p 是当前节点的前驱结点
    Node p = enq(node);
    int ws = p.waitStatus;
    // 前驱结点被取消,或者更改前驱结点状态为 signal 失败
    // 说明前驱节点已被取消,则直接唤醒当前线程
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

初看这段代码有一个疑惑,如果 if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) 这个条件不满足,那岂不是没有唤醒线程??

其实不然,signal() 方法的作用其实只是把等待队列中第一个非取消节点转移到 AQS 的同步队列尾部。转移后的节点很可能正在在同步队列阻塞着,什么时候唤醒,取决于它的前驱节点是否是头节点。但对于用户而言,调用 signal() 方法的时候,线程就已经被唤醒。所以这里很容易产生误解,就是其他线程执行 signal() 方法的时候,等待队列中第一非取消个节点会立马被唤醒(LockSupport.unpark()),现在看来,并不是。

if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) 这个判断条件有何作用?

ws 的值是转移到同步队列中节点前驱节点的状态值,当满足 if 中任意条件,则说明前驱节点已被取消。然后唤醒节点,即意味着 Condition#await() 方法将继续执行

// 此时,节点已在同步队列,跳出循环
while (!isOnSyncQueue(node)) {
    LockSupport.park(this);
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
        break;
}
// 接着获取同步状态
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
    interruptMode = REINTERRUPT;

节点被唤醒后将重新获取同步状态,如果获取失败,则执行 shouldParkAfterFailedAcquire() 方法,这个方法里面会根据前驱节点的状态做出不同的操作。如果前驱节点被取消,则把它从同步队列中移除掉,也就是下面这段代码

// ws 为前驱节点的状态值
if (ws > 0) {
    do {
        node.prev = pred = pred.prev;
    } while (pred.waitStatus > 0);
    pred.next = node;
} else {
    ...
}

到这里,也就是明白 if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) 这段代码的作用了:预先将同步队列中取消的节点移除掉,而不用等到获取同步状态失败的时候再去判断了,起到一定的优化作用。

signalAll

唤醒所有

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

// 转移等待队列中所有节点
private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}

总结

到此,ConditionObject 的核心方法的原理基本分析完毕,总结为以下几点

  1. await() 和 signal() 调用之前必须获取到同步状态(锁)
  2. await() 的逻辑:添加节点到等待队列的尾部,释放同步状态(锁),然后阻塞当前线程
  3. signal() 的逻辑:把等待队列中第一个非取消节点转移到 AQS 的同步队列尾部

另外,ConditionObject 还有一些其他的方法,比如 awaitNanos(long nanosTimeout)、awaitUntil(Date deadline) 和 await(long time, TimeUnit unit) 等,大体逻辑都是基于 await() 衍生而来,感兴趣的朋友可以另行研究。

参考

posted @ 2022-06-08 18:34  Tailife  阅读(90)  评论(0编辑  收藏  举报