《Java 并发编程·JUC 源码与容器》2.2 Condition 的条件队列

JDK 25 的 ConditionObject 用 ConditionNode 串起条件队列:await 先 enableWait 入队并释放全部重入次数,signal 只把节点 enqueue 到同步队列而不唤醒。用双条件 notEmpty/notFull 手写有界队列,并用实测时间线证明 signal 后线程仍在 park。

承接的问题

2.1 讲的是锁的同步队列:拿不到锁的线程在 AQS 里排队。Condition 引出第二条队列——条件队列。线程因为“某个条件还不满足”主动让出锁,进入条件队列;另一个线程改变条件后把它转移回同步队列,它才有资格重新竞争锁。

卷一 5.1 已经讲过 wait/notify 的用法和“必须在循环里等待”的规则,5.3 用监视器写过有界缓冲区。本节不重复这些,只回答源码问题:

  • ConditionObject 用什么结构保存等待者?
  • await() 从释放锁到重新拿到锁,中间经过哪几步?
  • 为什么说 signal() 只是“转移”,不是“唤醒”?

版本差异同样要先说清楚。JDK 8 的 ConditionObject 用 Node 的 CONDITION 状态和 transferForSignal() / transferAfterCancelledWait() 完成转移。JDK 25 已重构:条件队列改用独立的 ConditionNode,状态位是 COND + WAITING,转移直接调用 enqueue();transferForSignal 这个方法名在 JDK 25 源码里已经不存在。读旧笔记时要把这条映射记住。

条件队列的结构

ConditionObject 只有两个字段,指向一条单向链表:

// JDK 25 源码摘录,已删减
public class ConditionObject implements Condition, java.io.Serializable {
    private transient ConditionNode firstWaiter;
    private transient ConditionNode lastWaiter;
}

节点类型是 AQS 的静态嵌套类 ConditionNode,它继承 Node,额外维护 nextWaiter,并且实现 ForkJoinPool.ManagedBlocker:

// JDK 25 源码摘录,已删减
static final class ConditionNode extends Node
    implements ForkJoinPool.ManagedBlocker {
    ConditionNode nextWaiter;            // link to next waiting node
}

三个设计点:

  • 条件队列是单向的,只用 nextWaiter;同步队列是双向的,用 prev/next。条件队列的插入和删除都发生在持有锁的临界区里,不需要 CAS,单向链表足够。
  • ConditionNode 复用 Node 的 status,但语义不同:COND = 2 表示还在条件队列,WAITING = 1 表示节点处于可被 signal 或 park 的状态。enableWait 写入的是 COND | WAITING;signal 清掉 COND 后,节点仍带 WAITING 进入同步队列。getAndUnsetStatus(COND) 是 signal 和 await 竞争同一个节点的原子开关。
  • 实现 ManagedBlocker 是为了 ForkJoinPool:条件等待可能阻塞工作线程,managedBlock 允许池子临时补偿一个线程。这个行为从 JDK 9 开始存在,JDK 25 仍然只对无超时的 await() 生效,awaitNanos 不接这条路。

await 的四步

await() 的主体可以拆成四步:建节点、入条件队列并释放锁、阻塞等待、重新获取锁。先看“入队并释放”:

// JDK 25 源码摘录,已删减
private int enableWait(ConditionNode node) {
    if (isHeldExclusively()) {
        node.waiter = Thread.currentThread();
        node.setStatusRelaxed(COND | WAITING);
        ConditionNode last = lastWaiter;
        if (last == null)
            firstWaiter = node;
        else
            last.nextWaiter = node;
        lastWaiter = node;
        int savedState = getState();
        if (release(savedState))
            return savedState;
    }
    node.status = CANCELLED; // lock not held or inconsistent
    throw new IllegalMonitorStateException();
}

注意 release(savedState) 的参数:它释放的是当前线程持有的全部重入次数,不是 1。线程重入 3 层后调用 await(),会把 state 从 3 直接减到 0,唤醒同步队列里的后继;返回的 savedState 保存下来,等它被唤醒后再一次性恢复到 3。这就是 await() 不要求“只能重入一层”的原因。

// JDK 25 源码摘录,已删减
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    ConditionNode node = newConditionNode();
    if (node == null)
        return;
    int savedState = enableWait(node);
    LockSupport.setCurrentBlocker(this); // for back-compatibility
    boolean interrupted = false, cancelled = false, rejected = false;
    while (!canReacquire(node)) {
        if (interrupted |= Thread.interrupted()) {
            if (cancelled = (node.getAndUnsetStatus(COND) & COND) != 0)
                break;              // else interrupted after signal
        } else if ((node.status & COND) != 0) {
            try {
                if (rejected)
                    node.block();
                else
                    ForkJoinPool.managedBlock(node);
            } catch (RejectedExecutionException ex) {
                rejected = true;
            } catch (InterruptedException ie) {
                interrupted = true;
            }
        } else
            Thread.onSpinWait();    // awoke while enqueuing
    }
    LockSupport.setCurrentBlocker(null);
    node.clearStatus();
    reacquire(node, savedState);
    if (interrupted) {
        if (cancelled) {
            unlinkCancelledWaiters(node);
            throw new InterruptedException();
        }
        Thread.currentThread().interrupt();
    }
}

逐段对应:

  1. newConditionNode() 创建节点;如果内存不足,它会释放锁、退避 10 ms、重新获取锁,然后返回 null,让 await() 直接返回。这是 JUC 在 OOM 下的兜底,不是常规路径。
  2. enableWait(node) 把节点挂到 lastWaiter 后面,保存 savedState,并 release(savedState) 释放锁。
  3. 循环条件 !canReacquire(node):只要节点还没被成功转移到同步队列,就继续等。COND 位还在时,用 managedBlock 或 node.block() 阻塞;COND 位被清掉但节点尚未完成入队时,自旋等链接完成。
  4. reacquire(node, savedState) 把线程重新放回同步队列,并按 savedState 恢复重入次数。
  5. 中断处理分两种:如果中断发生在 signal 之前,getAndUnsetStatus(COND) 成功,节点从条件队列摘除并抛 InterruptedException;如果中断发生在 signal 之后,节点已经转移,先把中断标志补回去,让调用方在拿到锁后自己处理。

canReacquire、unlinkCancelledWaiters、reacquire 分别负责“是否已转移”“清理取消节点”“重新入同步队列”;旧版对应 isOnSyncQueue、unlinkCancelledWaiters、acquireQueued,JDK 25 换了实现但职责没变。

signal 只转移,不唤醒

signal() 做的事情比名字听起来少:

// JDK 25 源码摘录,已删减
private void doSignal(ConditionNode first, boolean all) {
    while (first != null) {
        ConditionNode next = first.nextWaiter;
        if ((firstWaiter = next) == null)
            lastWaiter = null;
        else
            first.nextWaiter = null; // GC assistance
        if ((first.getAndUnsetStatus(COND) & COND) != 0) {
            enqueue(first);
            if (!all)
                break;
        }
        first = next;
    }
}

public final void signal() {
    ConditionNode first = firstWaiter;
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    else if (first != null)
        doSignal(first, false);
}

signal() 先检查当前线程是否持有锁,然后取队首节点,清掉 COND 位,调用 enqueue(first) 把节点接到同步队列尾部。仅此而已。enqueue 正常路径只做 prev 赋值、tail CAS、next 链接;只有在前驱已经取消或初始化失败时才 unpark。所以被 signal 的线程不会立刻醒来:它已经具备竞争锁的资格,但要等 signaler 释放锁,由释放方唤醒同步队列的后继,它才真正从 park 返回。

这也解释了 JDK 8 的 transferForSignal 为什么叫“transfer”:它只负责把节点从条件队列搬到同步队列,唤醒是释放锁那一步的事。JDK 25 把这个动作内联进 doSignal 的 enqueue,名字没了,语义没变。

用时间线验证最清楚。主线程持锁,等待线程 await(),主线程 signal() 后不释放锁,观察等待线程的状态:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public final class ConditionFlow {
    public static void main(String[] args) throws Exception {
        ReentrantLock lock = new ReentrantLock();
        Condition cond = lock.newCondition();
        CountDownLatch aboutToAwait = new CountDownLatch(1);
        Thread waiter = Thread.ofPlatform().name("waiter").start(() -> {
            lock.lock();
            try {
                aboutToAwait.countDown();
                cond.await();
                System.out.println("waiter: after await, holdCount=" + lock.getHoldCount());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
            }
        });
        aboutToAwait.await();
        while (true) {
            lock.lock();
            if (lock.getWaitQueueLength(cond) > 0) break;
            lock.unlock();
            Thread.onSpinWait();
        }
        TimeUnit.MILLISECONDS.sleep(20);
        System.out.println("before signal: waitQueue=" + lock.getWaitQueueLength(cond)
                + " state=" + waiter.getState());
        cond.signal();
        System.out.println("after signal: waitQueue=" + lock.getWaitQueueLength(cond)
                + " state=" + waiter.getState());
        TimeUnit.MILLISECONDS.sleep(100);
        System.out.println("100ms after signal: state=" + waiter.getState()
                + " isLocked=" + lock.isLocked());
        lock.unlock();
        waiter.join();
        System.out.println("done, isLocked=" + lock.isLocked());
    }
}
before signal: waitQueue=1 state=WAITING
after signal: waitQueue=0 state=WAITING
100ms after signal: state=WAITING isLocked=true
waiter: after await, holdCount=1
done, isLocked=false

waitQueue 从 1 变 0,说明节点已经离开条件队列;state 仍是 WAITING,100 ms 后仍然 WAITING,说明它没有被唤醒。直到主线程 unlock 之后,等待线程才重新拿到锁并打印 holdCount=1。这就是“signal 只转移”的直接证据。

双条件有界队列

条件队列最典型的用法是生产者/消费者:一个条件等“不满”,一个条件等“不空”。用两个 Condition 而不是一个,可以只唤醒该醒的那一边。下面的实现用 capacity = 2、一个生产者放 5 个、一个消费者每 40 ms 取一个来验证:

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public final class BoundedQueue<T> {
    private final Deque<T> items = new ArrayDeque<>();
    private final int capacity;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();
    private final Condition notFull = lock.newCondition();

    public BoundedQueue(int capacity) {
        this.capacity = capacity;
    }

    public void put(T item) throws InterruptedException {
        lock.lockInterruptibly();
        try {
            while (items.size() == capacity) {
                notFull.await();
            }
            items.addLast(item);
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            while (items.isEmpty()) {
                notEmpty.await();
            }
            T item = items.removeFirst();
            notFull.signal();
            return item;
        } finally {
            lock.unlock();
        }
    }

    public int size() {
        lock.lock();
        try {
            return items.size();
        } finally {
            lock.unlock();
        }
    }
}
producer: put item-1, size=1
producer: put item-2, size=2
producer: put item-3, size=2
consumer: take -> item-1, size=1
consumer: take -> item-2, size=1
producer: put item-4, size=2
consumer: take -> item-3, size=1
producer: put item-5, size=2
consumer: take -> item-4, size=1
consumer: take -> item-5, size=0
done, final size=0

驱动是一个生产者线程循环 put 5 次、一个消费者线程每 40 ms take 一次,队列类未改一行。item-3 的 put 没有出现在 item-2 之后立刻完成,而是等到消费者取走 item-1 才继续,说明生产者确实在 notFull 上等待过。容量始终没有超过 2;put 返回到打印 size() 之间消费者可能已经取走元素,item-3 那行的 size=2 不是插入瞬间的快照,不能把 size() 当线性一致读数。

在 libraryd-conc 里落地

把 Condition 用在库存等待上:借阅请求在库存为 0 时不必立刻失败,可以按调用方给的预算等一小段时间;补货操作改变库存后 signalAll() 唤醒等待者。卷一 9.2 的 LockedLoanService 已经有 lock、books、loans 字段,这里新增一个条件:

// LockedLoanService 中新增的字段与方法
private final Condition stockChanged = lock.newCondition();

public Reservation reserveWaiting(String isbn, String memberId,
                                  long timeout, TimeUnit unit) {
    lock.lock();
    try {
        long nanos = unit.toNanos(timeout);
        Book book = books.findByIsbn(isbn)
                .orElseThrow(() -> new LoanException(ErrorCode.BOOK_NOT_FOUND, isbn));
        while (book.stock() <= 0) {
            if (nanos <= 0L) {
                throw new LoanException(ErrorCode.OUT_OF_STOCK, isbn);
            }
            try {
                nanos = stockChanged.awaitNanos(nanos);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new LoanException(ErrorCode.INTERRUPTED, "等待库存时被中断");
            }
            book = books.findByIsbn(isbn)
                    .orElseThrow(() -> new LoanException(ErrorCode.BOOK_NOT_FOUND, isbn));
        }
        books.save(new Book(book.isbn(), book.title(), book.stock() - 1));
        String loanId = "loan-" + System.nanoTime();
        Instant dueAt = Instant.now().plusSeconds(86400);
        loans.save(new Loan(loanId, isbn, memberId, dueAt));
        return new Reservation(loanId, isbn, memberId, dueAt);
    } finally {
        lock.unlock();
    }
}

public void restock(String isbn, int delta) {
    lock.lock();
    try {
        Book book = books.findByIsbn(isbn)
                .orElseThrow(() -> new LoanException(ErrorCode.BOOK_NOT_FOUND, isbn));
        books.save(new Book(book.isbn(), book.title(), book.stock() + delta));
        stockChanged.signalAll();
    } finally {
        lock.unlock();
    }
}
  • while (book.stock() <= 0) 不能改成 if。awaitNanos 返回可能是因为超时、中断、虚假唤醒,也可能因为另一个消费者先拿走了库存;醒来后必须重新查库、重新判断。
  • awaitNanos 返回的是剩余纳秒,要赋回 nanos;用 await(timeout, unit) 则只返回 boolean,循环里还得自己算截止时间。两者都要求持有锁。
  • restock 用 signalAll() 而不是 signal():不同 ISBN 的等待者可能混在同一个条件队列里,只唤醒队首可能唤醒一个不关心该 ISBN 的线程,真正需要的人继续睡。

如果只需要一个标准的阻塞队列,直接用 ArrayBlockingQueue(7.1)比手写两个条件更稳:它已经把锁拆分、容量检查、中断和公平性都处理好了。这里手写是为了讲清条件队列,不是建议在生产里重造。

这个设计的代价

  • 丢失信号是最常见的错误。 signal 只有在等待者已经进入条件队列时才有效;如果条件成立发生在 await 之前,信号就丢了。所以状态判断和 await 必须在同一个锁的保护下,并且用 while 重新检查。
  • signal 必须持锁。 isHeldExclusively() 检查失败会抛 IllegalMonitorStateException;它不是一个可以从任意线程调用的“通知”方法。
  • 条件队列和同步队列不是一条队列。 signal 之后节点只是进入同步队列,仍要排队拿锁;如果 signaler 一直不释放锁,等待者会一直停在 WAITING,这正是上面时间线展示的行为。
  • await 会释放全部重入次数。 在 await 前后依赖 getHoldCount() 的代码会看到跳变;不要把“持锁深度”当作业务状态。
  • 不要用 Condition 做跨进程或异步通知。 它绑定在一个 Lock 实例上,只在同进程的线程之间有效;跨进程用消息队列,异步编排用 CompletableFuture(13.x)。ForkJoinPool 里用 awaitNanos 仍可能占住工作线程,ManagedBlocker 只覆盖无超时 await。

小结

  • ConditionObject 用 firstWaiter/lastWaiter 维护一条单向条件队列,节点是 ConditionNode,靠 COND 状态位和 signal 竞争。
  • await 的完整流程是:建节点 → enableWait 入条件队列并释放全部重入次数 → 阻塞等待 → reacquire 回同步队列并恢复计数。
  • signal 只清 COND 并把节点 enqueue 到同步队列,不 unpark;真正唤醒发生在释放锁那一步。JDK 25 已没有 transferForSignal,读旧资料时把它理解为 doSignal 里的 enqueue 即可。
  • 双条件队列能减少无效唤醒,但条件判断必须放在 while 里,且 signal 必须在持锁时调用。

阅读导航:上一节 2.1 ReentrantLock 的公平与非公平 | 下一节 2.3 锁的中断、超时与可重入

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《Java 消息中间件·事件驱动架构》附录 B 架构决策记录模板
  2. 《Java 消息中间件·事件驱动架构》附录 A 术语表
  3. 《Java 消息中间件·事件驱动架构》15.3 全书验收