线程间通信
线程间通信用于让多个线程协调执行顺序和共享状态变化。典型场景是生产者消费者:队列为空时消费者等待,队列满时生产者等待。
# 1. wait/notify 模型
wait、notify、notifyAll 是 Object 的方法,必须在持有对象 monitor 时调用。
synchronized (lock) {
while (!condition) {
lock.wait();
}
doWork();
}
通知:
synchronized (lock) {
condition = true;
lock.notifyAll();
}
# 2. WaitSet 与 EntryList
Monitor
├─ Owner:持锁线程
├─ EntryList:等待获取锁的线程
└─ WaitSet:调用 wait 后等待通知的线程
调用 wait():
线程持有锁
│
▼
调用 wait
│
├─ 释放锁
└─ 进入 WaitSet
调用 notifyAll():
唤醒 WaitSet 中线程
│
▼
被唤醒线程进入锁竞争
│
▼
重新获得锁后从 wait 返回
被通知不等于立即执行,它还要重新竞争锁。
# 3. 为什么用 while
不推荐:
if (!condition) {
lock.wait();
}
推荐:
while (!condition) {
lock.wait();
}
原因:
- 可能虚假唤醒。
- 多个线程被唤醒后,条件可能已被其他线程消费。
- 被唤醒到重新拿锁之间,条件可能变化。
等待条件必须循环检查。
# 4. notify 还是 notifyAll
| 方法 | 行为 | 风险 |
|---|---|---|
notify | 唤醒一个等待线程 | 可能唤醒错误类型线程 |
notifyAll | 唤醒所有等待线程 | 唤醒开销更大 |
复杂条件下优先 notifyAll,避免信号丢失或唤醒错误线程导致系统卡住。
# 5. 生产者消费者示例
public class SimpleQueue<T> {
private final Queue<T> queue = new ArrayDeque<>();
private final int capacity;
public SimpleQueue(int capacity) {
this.capacity = capacity;
}
public synchronized void put(T item) throws InterruptedException {
while (queue.size() == capacity) {
wait();
}
queue.offer(item);
notifyAll();
}
public synchronized T take() throws InterruptedException {
while (queue.isEmpty()) {
wait();
}
T item = queue.poll();
notifyAll();
return item;
}
}
实际开发中优先使用 BlockingQueue,不要重复造队列。
# 6. Condition
Condition 是 Lock 体系中的条件队列。
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
相比一个对象只有一个 WaitSet,Condition 可以有多个等待队列。
ReentrantLock
├─ Condition notEmpty
└─ Condition notFull
这样可以更精准唤醒生产者或消费者。
# 7. park/unpark
LockSupport.park() 和 unpark() 是更底层的阻塞唤醒工具。
LockSupport.park();
LockSupport.unpark(thread);
特点:
- 不要求先持有 synchronized 锁。
unpark可以先于park调用,许可会保留一次。- AQS、线程池、并发工具底层大量使用它。
普通业务代码很少直接使用。
# 8. 更高级通信工具
| 工具 | 场景 |
|---|---|
BlockingQueue | 生产者消费者 |
CountDownLatch | 等待多个任务完成 |
CyclicBarrier | 多线程互相等待到屏障 |
Semaphore | 控制并发许可数 |
CompletableFuture | 异步任务编排 |
优先使用高层并发工具,少直接写 wait/notify。
# Tips 快问快答
Q:wait 为什么必须在 synchronized 中调用? A:线程必须持有对象 monitor,才能释放它并进入 WaitSet。
Q:wait 会释放锁吗? A:会释放当前对象的 monitor。
Q:sleep 会释放锁吗? A:不会。
Q:notify 后线程会立即执行吗? A:不会,被唤醒线程还要重新竞争锁。
Q:为什么等待条件用 while? A:防止虚假唤醒和条件被其他线程改变。
Q:notify 和 notifyAll 怎么选? A:复杂条件优先 notifyAll,避免唤醒错误线程。
Q:生产者消费者推荐手写 wait/notify 吗?
A:不推荐,优先使用 BlockingQueue。
Q:Condition 相比 wait/notify 有什么优势? A:可以为一把锁创建多个条件队列,唤醒更精准。