Appearance
第25课:线程通信
🎯 学习目标
- 掌握 wait/notify/notifyAll 的正确用法与"为什么必须在 synchronized 内"
- 掌握 Condition 条件变量(配合 Lock)
- 掌握并发工具:CountDownLatch、CyclicBarrier、Semaphore
- 用生产者-消费者模式串起通信机制
- 识别陷阱(虚假唤醒、wait 释放锁、await 必须 await+signal)
📖 一、概念讲解:线程通信
1. 为什么需要通信
线程同步解决"互斥"(不并发改同一数据)。但有时线程需要协作:一个线程要等另一个线程"准备好"——如生产者放数据后通知消费者、消费者取走后通知生产者。
这就是线程通信:一个线程让另一个线程"等待"或"继续"。
2. 两种核心机制
- wait/notify:配合 synchronized,基于对象 monitor。
- Condition(await/signal):配合 Lock,更灵活(可多个条件变量)。
3. 并发工具类(更高层抽象)
手写 wait/notify 易错,JUC 提供工具类:
- CountDownLatch:一次性倒计时,等 N 个线程完成。
- CyclicBarrier:可复用屏障,N 个线程互相等。
- Semaphore:信号量,控制并发数。
📖 二、wait/notify
java
class Buffer {
private Queue<Integer> queue = new LinkedList<>();
private final int CAP = 5;
public synchronized void put(int x) throws InterruptedException {
while (queue.size() == CAP) { // 用 while 不用 if(防虚假唤醒)
wait(); // 释放锁,等待被唤醒
}
queue.offer(x);
notifyAll(); // 唤醒所有等待的消费者
}
public synchronized int take() throws InterruptedException {
while (queue.isEmpty()) {
wait();
}
int x = queue.poll();
notifyAll(); // 唤醒所有等待的生产者
return x;
}
}关键规则(必须遵守)
- wait/notify 必须在 synchronized 块内,且锁的对象与 wait 的对象一致。否则抛
IllegalMonitorStateException。 - wait 会释放锁,被唤醒后重新获取锁才继续(这是与 sleep 最大区别——sleep 不释放锁)。
- 用 while 不用 if 判断条件:wait 被唤醒后条件可能已被其他线程改变(虚假唤醒),必须再次检查。
- notify 唤醒一个,notifyAll 唤醒所有。优先 notifyAll(避免"信号丢失"),优化时才用 notify。
为什么 wait 必须释放锁
wait 的目的是"我条件不满足,先让出锁给别人"。若不释放锁,其他线程无法修改条件,自己永远等不到——死锁。所以 wait = 释放锁 + 阻塞;被唤醒 = 重新竞争锁 + 继续。
📖 三、Condition
java
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
public void put(int x) throws InterruptedException {
lock.lock();
try {
while (queue.size() == CAP) notFull.await(); // 释放锁等待
queue.offer(x);
notEmpty.signal(); // 只唤醒消费者
} finally {
lock.unlock();
}
}Condition vs wait/notify
- 多个条件变量:wait/notify 只能一个等待队列;Condition 可有 notFull/notEmpty 等多个,signal 精确唤醒某一类。
- 配合 Lock:Condition 是 Lock 的产物(lock.newCondition())。
- API:await(等)/ signal(唤醒一个)/ signalAll(唤醒所有)。
典型场景:生产者-消费者用两个 Condition,生产者只唤醒消费者、消费者只唤醒生产者,比 notifyAll 更精确。
📖 四、CountDownLatch(一次性等待)
java
CountDownLatch latch = new CountDownLatch(3); // 计数3
// 3 个工作线程各 countDown 一次
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 干活
latch.countDown(); // 计数-1
}).start();
}
latch.await(); // 主线程阻塞直到计数归0
System.out.println("全部完成");特点:一次性,计数到 0 不能重置。一个线程等多个线程完成。常用于"主线程等所有子任务"。
📖 五、CyclicBarrier(可复用屏障)
java
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("都到了,继续"));
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 第一阶段
barrier.await(); // 等3个线程都到这,然后一起继续
// 第二阶段
}).start();
}特点:N 个线程互相等,都到达屏障点后一起继续;可复用(自动重置)。常用于"多线程分阶段并行计算"。
vs CountDownLatch:CyclicBarrier 是"线程互相等"且可复用;CountDownLatch 是"一个线程等其他线程"且一次性。
📖 六、Semaphore(信号量)
java
Semaphore sem = new Semaphore(3); // 3 个许可
new Thread(() -> {
sem.acquire(); // 获取许可(许可-1),无许可则阻塞
try {
// 临界区(最多3个线程同时)
} finally {
sem.release(); // 释放许可(许可+1)
}
}).start();用途:控制同时访问某资源的线程数(限流)。如数据库连接池大小、接口并发限制。
⚠️ 七、常见陷阱
陷阱1:wait 不在 synchronized 内
java
synchronized(obj){} // 块外
obj.wait(); // ❌ IllegalMonitorStateException原因:wait 要操作对象的 monitor(等待队列),必须持有 monitor(即持有对象锁)才能操作。
陷阱2:用 if 不用 while 判断条件
java
if (queue.isEmpty()) wait(); // ❌ 虚假唤醒时条件仍不满足却继续修复:while (queue.isEmpty()) wait();。Java 规范明确要求用 while。
陷阱3:用 notify 而非 notifyAll 导致信号丢失
多消费者场景,notify 可能唤醒同类消费者而非生产者,导致死锁。优先 notifyAll。
陷阱4:await 不在 lock 内
java
condition.await(); // ❌ 必须先 lock.lock() 持有锁陷阱5:signal 不唤醒
signal 唤醒后等待线程要重新竞争锁,若锁被长时间持有仍会等。
🆚 八、Java vs C / 工具选择
| 需求 | C 语言 | Java |
|---|---|---|
| 等待/唤醒 | pthread_cond_wait/signal | wait/notify 或 Condition |
| 一次性等待 | — | CountDownLatch |
| 互等屏障 | pthread_barrier | CyclicBarrier |
| 并发数控制 | 信号量 sem_t | Semaphore |
选择优先级:
- 优先用 JUC 工具类(CountDownLatch/CyclicBarrier/Semaphore)—— 封装好、不易错。
- 生产者-消费者用 BlockingQueue(封装了 wait/notify)—— 比手写安全。
- 手写 wait/notify 仅在学习或特殊场景。
💡 九、最佳实践
- wait/notify 必须在 synchronized 内,锁对象一致。
- while 判断条件,防虚假唤醒。
- 优先 notifyAll,优化时才 notify。
- 优先用 BlockingQueue/工具类,少手写。
- Condition 配合 Lock,多条件变量更精确。
- await 在 lock 内、signal 后 unlock。
📝 练习预告
完成 练习/Ex25_ThreadComm.java 中的 6 道题:
- wait/notify 基本通信
- 生产者-消费者(wait/notifyAll)
- Condition 精确唤醒
- CountDownLatch 等待多任务
- CyclicBarrier 多线程分阶段
- 综合:用 Semaphore 实现限流器
完成后对比 答案/Sol25.java,查看逐行讲解与多解法。
📖 十、等待队列与锁的关系
每个 Java 对象都可以作为 monitor。
当线程执行:
java
synchronized (lock) {
lock.wait();
}发生的事情:
text
线程必须先持有 lock 的 monitor。
调用 wait 后释放 monitor。
线程进入 lock 的等待队列。
其他线程 synchronized(lock) 后可以修改条件。
其他线程调用 notify/notifyAll。
等待线程被唤醒后重新竞争 monitor。
拿到 monitor 后从 wait 后继续执行。所以 wait/notify 的对象必须和 synchronized 锁对象一致。
错误示例:
java
synchronized (a) {
b.wait(); // 锁对象不是 b,抛 IllegalMonitorStateException
}📖 十一、超时等待与中断
线程通信必须考虑退出路径。
wait 支持超时:
java
while (!ready) {
lock.wait(1000);
}Condition 也支持超时:
java
while (!ready) {
if (!condition.await(1, TimeUnit.SECONDS)) {
throw new TimeoutException();
}
}处理中断:
java
try {
queue.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}不要吞掉中断。中断通常表示“请尽快停止当前等待或任务”。
📊 十二、工具类选型
| 需求 | 推荐 |
|---|---|
| 等多个任务完成 | CountDownLatch |
| 多个线程分阶段互等 | CyclicBarrier |
| 控制同时访问数量 | Semaphore |
| 生产者消费者 | BlockingQueue |
| 多条件精确等待 | Condition |
| 简单对象等待通知 | wait/notify |
| 异步任务结果 | Future / CompletableFuture |
选择原则:
text
能用高层工具,就不要手写 wait/notify。
需要队列通信,用 BlockingQueue。
需要事件完成等待,用 CountDownLatch。
需要限流,用 Semaphore。
需要复杂锁条件,用 Lock + Condition。🧪 十三、案例:启动门闩
多个工作线程等待统一开始:
java
CountDownLatch start = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
start.await();
doWork();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
done.countDown();
}
}).start();
}
start.countDown();
done.await();这个模式常用于压测、并发测试和批量任务启动。
🛠 十四、线程通信排查清单
常见问题:
text
wait/notify 不在 synchronized 内。
wait 对象和锁对象不一致。
使用 if 而不是 while。
notify 唤醒了错误类型线程。
signal 后仍长时间持有锁。
await 中断被吞。
CountDownLatch 某个分支忘记 countDown。
Semaphore acquire 后异常路径未 release。
Barrier 中某个线程失败导致其他线程永久等待。排查建议:
text
给等待加超时。
线程 dump 查看 WAITING/BLOCKED 状态。
finally 中释放许可或 countDown。
日志记录进入等待和被唤醒的条件。
优先使用并发工具类减少手写协议。✅ 十五、掌握标准
学完本课后,应能做到:
text
能解释 wait 为什么必须在 synchronized 中。
能说明 wait 会释放锁而 sleep 不会。
能用 while 防止虚假唤醒。
能用 Condition 实现多条件等待。
能区分 CountDownLatch 和 CyclicBarrier。
能用 Semaphore 控制并发数量。
能用 BlockingQueue 替代手写生产者消费者。
能正确处理中断和超时等待。
能通过线程 dump 判断线程是在 WAITING、TIMED_WAITING 还是 BLOCKED。线程通信本质是“条件不满足就等待,条件改变后通知”。正确性依赖条件判断、锁对象一致和退出路径完整。
🎓 下一步
- 第26课:线程池 — Executor、ThreadPoolExecutor、参数调优