跳转至

AQS 队列如何唤醒阻塞线程

一、线程怎么挂起

acquireQueued 里,前驱不是 head 时,线程挂起:

private static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

// 在 parkAndCheckInterrupt 里
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);   // 挂起当前线程
    return Thread.interrupted();
}

LockSupport.park() 调用 Unsafe.park(),线程进入 WAITING 状态,不占 CPU。

二、怎么唤醒

锁释放时,releaseunparkSuccessor(head)

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);   // 唤醒后继
}

流程

  1. 找 head 的下一个有效 node。
  2. LockSupport.unpark(thread) 唤醒。
  3. 被唤醒的线程从 park() 返回,继续自旋 tryAcquire。

三、为什么从尾往前找

因为入队时 next 指针是后加的,可能读到 null。从 tail 往前找 prev 更安全。

四、被唤醒后做什么

final boolean acquireQueued(Node node, int arg) {
    for (;;) {
        final Node p = node.predecessor();
        if (p == head && tryAcquire(arg)) {   // 前驱是 head,再抢一次
            setHead(node);
            p.next = null;
            return interrupted;
        }
        // 不该抢,继续 park
    }
}

被唤醒后,检查前驱是不是 head: - 是:自己成为队列头,拿到锁。 - 不是:继续 park。

五、为什么不直接唤醒

锁释放后,可能有多个线程在等,但只唤醒一个(独占锁)。让队列里第一个去抢,抢到了就往下传。

park/unpark

  • LockSupport.park():挂起当前线程。
  • LockSupport.unpark(thread):唤醒指定线程。
  • 基于 Unsafe.park/unpark,不依赖 Object 的 wait/notify。