源码解析 AQS 之 Condition

本篇文章最好结合《深入剖析 ReentrantLock》一起看

在使用 synchronized 保证线程安全时,如果想要实现线程的等待/通知机制(手动阻塞和唤醒线程),我们可以使用 Object.wait()/Object.notify() 来操作,一般可以通过这两个方法实现一个自定义的基于 生产者/消费者模型 的阻塞队列。

而在上篇文章《深入剖析 ReentrantLock》我们了解到,Lock是基于 AQS (AbstractQueuedSynchronizer) 来实现 加锁/解锁 以及管理线程的 阻塞/唤醒

  • 加锁/解锁:volatile 修饰 state 来管理锁的状态,加锁解锁的操作是通过 CAS 操作 state 值实现;

  • 线程的阻塞/唤醒:维护一个先入先出的双向队列(下文称「同步队列」)来管理未抢到锁的线程,并且有一套机制来进行线程的阻塞和唤醒。

AQS 基于 ConditionObject 实现获取锁后线程的手动 阻塞/唤醒(注意,未获取到锁的线程调用 Condition 的 await() 和 signal() 相关的方法会抛出异常,下文会分析),ConditionObject 在 AQS 中以内部类的形式存在,各个 Lock 工具类都提供新建的入口,比如 ReentrantLock 中提供 newCondition() 方法创建 Condition:

final ConditionObject newCondition() {
    return new ConditionObject();
}

ConditionObject 内部维护了一个单向链表来实现线程的 等待/通知机制

// 条件队列头节点
private transient Node firstWaiter;
// 条件队列尾节点
private transient Node lastWaiter;

Node 结构:

static final class Node {
    // 节点状态,加入条件队列节点状态为-2
    volatile int waitStatus;
    // 「同步队列」节点的前继节点
    volatile Node prev;
    // 「同步队列」节点的后面继节点
    volatile Node next;
    // 线程
    volatile Thread thread;
    // 「条件队列」的后继节点
    Node nextWaiter;
}

一个 Condition 就维护着一个「条件队列」,不同 Condition 之间的「条件队列」是互相隔离的。相比于 synchronized,多个「条件队列」会让 唤醒/阻塞 的粒度更小。

Condition 中的阻塞机制

await() 方法是会让当前线程加入到调用者 Condition 的「条件队列」里,最后释放锁并阻塞线程,和 Object.wait() 方法基本一致。

await() 方法源码:

public final void await() throws InterruptedException {
    // 判断当前线程是否中断并清空中断标志
    if (Thread.interrupted())
        throw new InterruptedException();
    // 将当前线程加入到「条件队列」
    Node node = addConditionWaiter();
    // 释放所有锁,将state修改为0
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 判断当前node是否在「同步队列」内
    while (!isOnSyncQueue(node)) {
        // 进行线程阻塞
        LockSupport.park(this);
        // 线程被唤醒后执行到这里,先检查中断状态
        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() 方法的逻辑是:先判断线程的中断状态,接着将当前线程以 Codition 状态加入到 CoditionObject「条件队列」的尾部,然后释放锁,最后阻塞当前线程。后续线程被唤醒后会调用 acquireQueued() 争抢锁。

我们来看下内部的方法细节:

addConditionWaiter() 方法,加入「条件队列」尾部:

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // 如果「条件队列」尾部已取消就清理掉
    if (t != null && t.waitStatus != Node.CONDITION) {
        // 清理Cancelled状态节点
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // 初始化当前线程的CONDITION状态节点
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    // 加入「同步队列」尾部
    lastWaiter = node;
    return node;
}
// 清理所有取消的节点
private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        // 如果节点状态不为 -2,就断开节点
        if (t.waitStatus != Node.CONDITION) {
            t.nextWaiter = null;
            if (trail == null)
                firstWaiter = next;
            else
                trail.nextWaiter = next;
            if (next == null)
                lastWaiter = trail;
        }
        else
            trail = t;
        t = next;
    }
}

fullyRelease() 方法,释放锁,将 state 值修改为 0:

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState();
        // 将 state 修改为 0
        // 如果当前线程没有持有锁并调用await方法会抛出IllegalMonitorStateException异常
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            // 如果释放锁失败抛出异常
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            // 走到这里代表非持有锁的线程调用await方法或释放锁失败
            // 将节点状态设置为取消,等待后续懒加载的方式清理
            node.waitStatus = Node.CANCELLED;
    }
}

因为释放锁的过程无竞争,所以无需 CAS 操作 state 保证原子,直接修改即可。释放锁的时候如果发现非占用线程调用会抛出异常

acquireQueued() 方法,获取锁:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 自旋
        for (;;) {
            // 获取node节点的前继节点
            final Node p = node.predecessor();
            // 如果前继节点是头节点,就尝试获取锁(优化)
            if (p == head && tryAcquire(arg)) {
                // 获取成功将当前节点设置为头节点
                setHead(node);
                // 断开之前的头节点
                p.next = null; // help GC
                failed = false;
                // 返回代表获取锁成功,返回执行业务代码
                return interrupted;
            }
            // 如果前继节点不是头节点或获取锁失败会走到这里
            // 检查当前节点是否应该阻塞
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

你可能会发现 acquireQueued() 方法里争抢锁的前提是 node 节点需要在「同步队列」里,之前分析了调用 await() 方法是会把线程节点放到「条件队列」的尾部,那是何时放到「同步队列」的呢?我们看下 signal() 方法寻找下答案。

Condition 中的唤醒机制

signal() 方法会唤醒「条件队列」的头部节点去争抢锁:

public final void signal() {
    // 如果非持有锁的线程调用 signal 方法则会抛出异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        // 唤醒「条件队列」头节点
        doSignal(first);
}

doSignal() 方法的作用就是将「条件队列」中第一个非取消状态的节点移动到「同步队列」参与锁的竞争:

private void doSignal(Node first) {
    do {
        // 断开当前节点并遍历到下个节点
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    // 转移node节点到「同步队列」内,如果不成功执行到do代码块内
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

transferForSignal() 方法将 node 节点转移到「同步队列」内参与锁的争抢:

final boolean transferForSignal(Node node) {
    // 通过CAS去修改节点状态为初始化状态0
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        // 修改失败代表节点已经取消,返回false唤醒下一个节点
        return false;
    // 将当前节点加到「同步队列」尾部,并返回当前节点在「同步队列」中的前继节点
    Node p = enq(node);
    int ws = p.waitStatus;
    // 如果前继节点已经取消或设置唤醒状态失败则唤醒节点
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

transferForSignal() 方法就是 signal() 方法的核心,通过 CAS  的方式去修改节点状态为初始化状态 0(后续会从「条件队列」清理掉),然后转移到到「同步队列」的尾部。

可以看出来 Condition.signal() 方法的唤醒还是比较公平的,而 Object.notify() 是随机选一个线程进行唤醒,这也是两者最大的区别;源码里注释也是The choice is arbitrary:这个选择是随机的:

signalAll() 方法的作用是转移「条件队列」里所有非取消状态的节点到「同步队列」里参与争抢锁:

public final void signalAll() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        // 调用doSignalAll方法对「条件队列」从头开始遍历,转移到「同步队列」尾部
        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); // 直到遍历完成
}

Condition 中的超时阻塞机制

Condition 也支持调用 await 方法超时自动唤醒的 API:await(long, TimeUnit),awaitNanos(long),awaitUntil(Date)。

实现上都差不多,我们选择 await(long, TimeUnit) 来分析下:

public final boolean await(long time, TimeUnit unit)
        throws InterruptedException {
    // 计算超时纳秒的时间戳
    long nanosTimeout = unit.toNanos(time);
    // 如果线程中断抛出中断异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 将当前线程加入到「条件队列」
    Node node = addConditionWaiter();
    // 释放所有锁,将 state 修改为0,如果当前线程非锁的持有者就抛出异常
    int savedState = fullyRelease(node);
    // 计算超时时间戳
    final long deadline = System.nanoTime() + nanosTimeout;
    boolean timedout = false;
    int interruptMode = 0;
    // 如果节点不在「同步队列」里进入循环
    while (!isOnSyncQueue(node)) {
        // 判断是否达到超时时间
        if (nanosTimeout <= 0L) {
            // 如果是,将当前节点转移到「同步队列」尾部,参与锁的争抢
            timedout = transferAfterCancelledWait(node);
            break; // 跳出循环
        }
        // 如果超时时间大于等于 1000 纳秒就直接调用 LockSupport.parkNanos 方法超时阻塞
        if (nanosTimeout >= spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanosTimeout);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
        // 如果超时时间小于 1000 纳秒就自旋直到加入到「同步队列」尾部
        nanosTimeout = deadline - System.nanoTime();
    }
    // 参与争抢锁
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
    return !timedout;
}

await(long, TimeUnit) 方法的逻辑和之前《深入剖析 ReentrantLock》中讲的 tryLock(long, TimeUnit) 方法逻辑类似:

1. 计算出超时纳秒的时间戳;

2. 判断线程中断,如果中断抛出异常;

3. 将当前线程加入到「条件队列」;

4. 释放锁并校验调用线程的合法性(是否持有锁);

5. 再次计算超时时间戳并判断超时时间是否已到达,如果是就将线程节点转移到「同步队列」尾部;

6. 如果超时时间没到,就判断是否小于spinForTimeoutThreshold(1000纳秒),如果是进行自旋,否则阻塞(这里也同样是个优化,针对超时时间短的线程进行自旋来尝试加到「同步队列」尾部来减少阻塞带来的开销)。

Lock 的整体流程总结如下:

如果觉得文章不错可以点个赞和关注

公众号:阿东编程之路

你好,我是阿东,目前从事后端研发工作。技术更新太快需要一直增加自己的储备,索性就将学到的东西记录下来同时分享给朋友们。未来我会在此公众号分享一些技术以及学习笔记之类的。妥妥的都是干货,跟大家一起成长。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值