Skip to content

第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;
    }
}

关键规则(必须遵守)

  1. wait/notify 必须在 synchronized 块内,且锁的对象与 wait 的对象一致。否则抛 IllegalMonitorStateException
  2. wait 会释放锁,被唤醒后重新获取锁才继续(这是与 sleep 最大区别——sleep 不释放锁)。
  3. 用 while 不用 if 判断条件:wait 被唤醒后条件可能已被其他线程改变(虚假唤醒),必须再次检查。
  4. 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/signalwait/notify 或 Condition
一次性等待CountDownLatch
互等屏障pthread_barrierCyclicBarrier
并发数控制信号量 sem_tSemaphore

选择优先级

  1. 优先用 JUC 工具类(CountDownLatch/CyclicBarrier/Semaphore)—— 封装好、不易错。
  2. 生产者-消费者用 BlockingQueue(封装了 wait/notify)—— 比手写安全。
  3. 手写 wait/notify 仅在学习或特殊场景。

💡 九、最佳实践

  1. wait/notify 必须在 synchronized 内,锁对象一致。
  2. while 判断条件,防虚假唤醒。
  3. 优先 notifyAll,优化时才 notify。
  4. 优先用 BlockingQueue/工具类,少手写。
  5. Condition 配合 Lock,多条件变量更精确。
  6. await 在 lock 内、signal 后 unlock

📝 练习预告

完成 练习/Ex25_ThreadComm.java 中的 6 道题:

  1. wait/notify 基本通信
  2. 生产者-消费者(wait/notifyAll)
  3. Condition 精确唤醒
  4. CountDownLatch 等待多任务
  5. CyclicBarrier 多线程分阶段
  6. 综合:用 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、参数调优