承接的问题
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();
}
}
逐段对应:
newConditionNode()创建节点;如果内存不足,它会释放锁、退避 10 ms、重新获取锁,然后返回null,让await()直接返回。这是 JUC 在 OOM 下的兜底,不是常规路径。enableWait(node)把节点挂到lastWaiter后面,保存savedState,并release(savedState)释放锁。- 循环条件
!canReacquire(node):只要节点还没被成功转移到同步队列,就继续等。COND位还在时,用managedBlock或node.block()阻塞;COND位被清掉但节点尚未完成入队时,自旋等链接完成。 reacquire(node, savedState)把线程重新放回同步队列,并按savedState恢复重入次数。- 中断处理分两种:如果中断发生在
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 锁的中断、超时与可重入
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。