并发里所有“等待/唤醒”问题,最终都绕不开三套机制:

  • Object.wait/notify/notifyAll(监视器机制,配合 synchronized
  • Condition.await/signal/signalAll(显式锁配套)
  • LockSupport.park/unpark(更底层,AQS/FutureTask/线程池广泛使用)

写清楚它们的差异,很多“信号丢失/虚假唤醒/死等”的问题会一下子明朗。


1. 对比速查表

机制 等待条件 唤醒方式 是否必须持有锁 是否可能虚假唤醒 典型使用处
wait/notify 对象 monitor 的 wait-set notify/notifyAll 是(必须在 synchronized 内) 传统生产者-消费者、synchronized 协作
Condition 条件队列(每个 Condition 一个队列) signal/signalAll 是(必须持有关联 Lock) ReentrantLock 协作、多条件队列
LockSupport 线程的“permit” unpark(thread) 会(park 可能无理由返回) AQS、FutureTask、线程池阻塞

2. wait/notify:监视器上的等待集合

2.1 关键规则

  • 必须在 synchronized(obj) 内调用 obj.wait() / obj.notify()
  • wait() 会:
    • 释放 monitor
    • 把线程加入 obj 的等待集合
    • 被唤醒后重新竞争 monitor

2.2 正确姿势:永远用 while

1
2
3
4
5
6
synchronized (lock) {
while (!conditionOk()) {
lock.wait();
}
// condition ok
}

原因:

  • 虚假唤醒:线程可能在没有收到通知时返回
  • 竞争唤醒:notifyAll 后只有一个线程能拿到资源,其余线程需要继续等

2.3 notify vs notifyAll

  • notify():只随机唤醒一个等待线程,容易造成“唤醒了不该唤醒的人”(条件不满足又继续睡)
  • notifyAll():唤醒全部,依靠 while 自旋检查条件;更安全但唤醒风暴更大

3. Condition:更可控的等待/唤醒

Condition 的最大价值是:

  • 一个 Lock 可以派生多个 Condition
  • 每个 Condition 有自己的等待队列

这让“多条件协作”更清晰:

  • notEmpty
  • notFull
  • hasResource

3.1 模板

1
2
3
4
5
6
7
8
9
10
11
12
ReentrantLock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();

lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
// consume
} finally {
lock.unlock();
}

3.2 工程易错点

  • await()InterruptedException:要决定是“吞掉并继续”还是“回填标志并退出”(通常建议回填并退出)
  • signal() 一定要在持锁状态下调用(否则时序很难保证)

4. LockSupport:permit 语义(不会丢信号)

LockSupport 可以理解成:

  • 每个线程有一个 permit(0 或 1)
  • park():如果 permit=0 就阻塞;如果 permit=1 就消费 permit 并立即返回
  • unpark(t):把 t 的 permit 置为 1(如果已经是 1 就保持 1)

关键结论:

  • unpark 可以先于 park 调用,信号不会丢(permit 会被记住)

这也是它被 AQS 选为底层工具的重要原因。

4.1 但为什么还说可能“无理由返回”

  • park() 允许虚假返回:
    • 中断
    • 其它原因

所以用 park 的上层实现(AQS/FutureTask)都会在循环里反复检查条件。


5. “信号丢失”与“死等”常见成因

  • if 代替 while(虚假唤醒直接穿透)
  • 条件检查与入队/等待不是原子序列(检查完条件后再 wait,期间错过 notify)
  • 多条件混用一个 wait-set(notify 唤醒了不该唤醒的线程)

规避建议:

  • 用 Condition 拆分条件队列
  • 用 while 守护条件
  • 用更底层的并发结构(BlockingQueue)替代手写 wait/notify

6. 总结:怎么选

  • synchronized 协作:用 wait/notifyAll,并坚持 while
  • ReentrantLock:用 Condition(多条件更清晰)
  • 写同步器/框架代码:用 LockSupport(permit 语义更适合)

面试题 / Checklist

  • 为什么 wait()/notify() 必须在 synchronized 里调用?
  • 为什么必须用 while 而不是 if 来守护条件?
  • notify()notifyAll() 的取舍点是什么?
  • Condition 相比 wait/notify 解决了什么问题(多条件队列/精准唤醒)?
  • LockSupport 的 permit 语义是什么?为什么 unpark 可以先于 park
  • 什么是“信号丢失”?在手写等待/唤醒时如何避免?