线程之间有哪些通信方式?wait/notify 为什么要配合锁?
结论先行:Java 线程通信只有两大类——共享内存 + 同步(wait/notify、Condition、volatile 标志位)和消息传递(BlockingQueue、管道流)。wait/notify 必须配合锁,是因为"检查条件"和"进入等待"这两步必须原子,否则会丢唤醒。
1. 四种通信方式
| 方式 | 实现 | 适用 | 注意 |
|---|---|---|---|
wait/notify | Object 的监视器 | 经典生产者-消费者 | 必须在 synchronized 内,用 while 判条件 |
Condition | ReentrantLock.newCondition() | 需要多组等待队列 | await/signal 替代 wait/notify |
| 阻塞队列 | BlockingQueue | 生产消费解耦 | 生产环境首选,天然线程安全 |
| 管道 / 协调工具 | PipedInputStream、CountDownLatch、CompletableFuture | 流式传输、任务编排 | 各有适用场景,别硬套 |
2. 为什么 wait 必须持有锁
假设允许"不持锁就 wait",会出现这个漏洞:
时刻 t1 消费者检查 buffer 为空 → 准备 wait
时刻 t2 生产者放入元素 → notify()(此时没人等待,信号丢失)
时刻 t3 消费者执行 wait() → 永远等下去
"检查条件"与"进入等待"之间存在窗口。把这两步放进 synchronized 后,生产者要 notify 也必须先拿到同一把锁,于是不可能在窗口期插入——这就是 wait/notify 强制要求持有 monitor 的原因(不持有会抛 IllegalMonitorStateException)。
3. 标准模板:必须用 while 判断
public class BoundedBuffer<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
public BoundedBuffer(int capacity) { this.capacity = capacity; }
public synchronized void put(T item) throws InterruptedException {
while (queue.size() == capacity) { // 用 while,不是 if
wait(); // 释放锁并挂起
}
queue.add(item);
notifyAll(); // 唤醒所有等待者(含消费者)
}
public synchronized T take() throws InterruptedException {
while (queue.isEmpty()) {
wait();
}
T item = queue.poll();
notifyAll();
return item;
}
}
为什么是 while 而不是 if,有两个硬理由:
- 虚假唤醒(spurious wakeup):JVM 允许
wait()在没有notify的情况下返回,醒来后条件可能仍不满足; - 被唤醒后要重新竞争锁:在等待队列里醒来的线程不保证马上拿到锁,等它拿到时条件可能又变了。
4. 生产环境更推荐 BlockingQueue
BlockingQueue<Order> queue = new LinkedBlockingQueue<>(1000);
// 生产者
queue.put(order); // 队列满则阻塞,天然背压
// 消费者
Order o = queue.take(); // 队列空则阻塞
// 或者用线程池直接消费
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
Order o = queue.take();
handle(o);
}
});
BlockingQueue 内部已经把 ReentrantLock + 两个 Condition(notEmpty、notFull)封装好了,比自己写 wait/notify 少踩一堆坑。
5. 常见追问
追问一:notify 和 notifyAll 怎么选?
优先 notifyAll。notify 只唤醒一个线程,如果唤醒的是"条件不满足"的那类线程(比如生产者唤醒了另一个生产者),真正的消费者还在睡,就会假死。notifyAll 多一次无效唤醒,但安全。
追问二:wait 和 sleep 有什么区别?
wait 是 Object 方法、释放锁、需要 notify 唤醒、必须在同步块内;sleep 是 Thread 静态方法、不释放锁、只等超时。一句话:wait 让出锁等人叫,sleep 抱着锁睡到点。
追问三:Condition 比 wait/notify 好在哪?
一个 Lock 可以创建多个 Condition,实现精确唤醒。比如有界队列里 notEmpty 只唤醒消费者、notFull 只唤醒生产者,避免了 notifyAll 的惊群。
追问四:volatile 能做线程通信吗?
能做"标志位"式的单向通知(如 volatile boolean stop),但无法表达"等待某个条件成立"。需要挂起/唤醒时必须用 wait/notify、Condition 或 LockSupport.park/unpark。
追问五:LockSupport 和 wait/notify 的区别?
LockSupport.park/unpark 以线程为对象、不需要持有锁、unpark 可以先于 park 调用(有许可计数),不会丢信号;wait/notify 以对象 monitor 为对象、必须先持锁、信号可能丢失。
小结
线程通信的本质是"等一个条件成立"。wait/notify 把"判断条件 + 挂起"放进锁里保证原子,while 循环负责醒来后复查,BlockingQueue 则把这套逻辑封装成了开箱即用的组件。