JAVA重点基础、进阶知识及易错点总结(18)线程通信 wait/notify/notifyAll

张开发
2026/4/15 10:06:42 15 分钟阅读

分享文章

JAVA重点基础、进阶知识及易错点总结(18)线程通信 wait/notify/notifyAll
Java 巩固进阶 · 第18天主题线程通信 wait/notify/notifyAll —— 生产者消费者模型实战 进度概览今天学习多线程协作的核心机制线程通信。掌握 wait/notify你就能实现线程间的精准配合这是阻塞队列、消息队列、任务调度器的底层基石。 核心价值协作基石让线程从各自为战升级为团队配合实现高效的生产消费、任务分发。框架原理理解ArrayBlockingQueue、SpringBootAsync、Kafka 消费者组的底层通信机制。面试高频wait/notify 使用规则、虚假唤醒、生产者消费者模型是并发面试的必考题。设计思维学会用条件等待 通知唤醒替代忙等待大幅提升系统性能。一、为什么需要线程通信从盲等到精准通知 1. 经典场景生产者 - 消费者模型┌─────────────────────────────────────┐ │ 生产者线程 │ │ - 负责生成数据订单/日志/消息 │ │ - 数据放入共享缓冲区 │ │ - 缓冲区满时暂停生产等待消费 │ └─────────────────────────────────────┘ ↓ 共享缓冲区 ┌─────────────────────────────────────┐ │ 消费者线程 │ │ - 从缓冲区取数据进行处理 │ │ - 缓冲区空时暂停消费等待生产 │ │ - 处理完成通知生产者可继续生产 │ └─────────────────────────────────────┘ 核心诉求 ✅ 缓冲区空 → 消费者等待生产者生产后通知 ✅ 缓冲区满 → 生产者等待消费者消费后通知 ❌ 禁止忙等待while(缓冲区空) {空转} → 浪费CPU2. 真实业务场景映射业务场景生产者消费者共享缓冲区订单处理用户下单接口订单履约服务订单队列Redis/DB日志收集业务日志打印日志异步写入内存缓冲队列消息推送事件触发服务短信/邮件发送消息队列Kafka/RocketMQ数据同步binlog 监听ES/缓存同步Canal 通道核心思想“条件不满足时等待释放锁条件满足时通知唤醒等待者”用wait/notify替代sleep轮询让线程聪明地等待而非傻等。二、三大核心方法wait / notify / notifyAll 1. 方法签名 核心特性// ⚠️ 重要这三个方法定义在 Object 类所有对象都能调用publicfinalvoidwait()throwsInterruptedException;// 等待publicfinalvoidnotify();// 唤醒一个publicfinalvoidnotifyAll();// 唤醒全部2. 使用规则❗ 违反必报错规则说明违反后果必须在 synchronized 内调用因为需要持有锁才能操作等待队列IllegalMonitorStateExceptionwait() 会释放锁线程进入等待池其他线程可竞争锁✅ 核心特性实现线程协作被唤醒后需重新竞争锁notify 后被唤醒线程不会立即执行需重新获取锁⚠️ 注意唤醒≠执行InterruptedExceptionwait 可被中断必须捕获或抛出✅ 支持优雅停机3. 方法对比 选型建议synchronized(lock){// wait()当前线程释放锁进入等待池// - 直到被 notify/notifyAll 唤醒或 interrupt或 timeout// - 唤醒后重新竞争锁竞争成功才继续执行lock.wait();// notify()随机唤醒一个在 lock 上 wait 的线程// - 不释放当前锁当前线程执行完同步块后被唤醒者才有机会竞争// - 随机依赖 JVM 实现不可控慎用lock.notify();// notifyAll()唤醒所有在 lock 上 wait 的线程// - 所有被唤醒者竞争锁只有一个能执行其余继续等待// - ✅ 生产环境推荐避免唤醒错误线程导致的死锁/饥饿lock.notifyAll();}为什么生产环境推荐 notifyAll()场景缓冲区有空和满两个条件生产者和消费者都在 wait 如果用 notify() - 生产者生产完调用 notify()可能唤醒另一个生产者而非消费者 - 被唤醒的生产者发现缓冲区仍满继续 wait → 消费者永远不被唤醒 → 死锁 如果用 notifyAll() - 所有等待者都被唤醒各自检查条件 - 消费者发现缓冲区有数据执行消费生产者发现仍满继续 wait - ✅ 虽然多唤醒几个线程但逻辑正确避免死锁三、经典范式条件等待 通知唤醒⭐ 背下来1. 标准模板直接复用synchronized(lock){// 步骤1while 循环检查条件❗ 必须用 while不能用 ifwhile(条件不满足){// 如buffer.isEmpty() / buffer.isFull()try{lock.wait();// 释放锁进入等待}catch(InterruptedExceptione){// ✅ 中断处理恢复中断标志 优雅退出Thread.currentThread().interrupt();return;// 或 throw new RuntimeException(e)}}// ✅ 步骤2条件满足执行核心业务doBusinessLogic();// 步骤3状态改变通知其他等待线程lock.notifyAll();// ✅ 推荐用 notifyAll避免唤醒错误线程}2. ❗ 为什么必须用while而不是if// ❌ 错误用 if 判断可能遭遇虚假唤醒synchronized(lock){if(buffer.isEmpty()){// 只判断一次lock.wait();// 被唤醒后直接往下执行不再检查条件}// 风险被唤醒时缓冲区可能仍为空虚假唤醒/其他线程抢占buffer.take();// 可能抛出 NoSuchElementException}// ✅ 正确用 while 循环唤醒后重新检查条件synchronized(lock){while(buffer.isEmpty()){// 每次唤醒都重新检查lock.wait();}// ✅ 安全能执行到这里说明缓冲区一定非空buffer.take();} 什么是虚假唤醒Spurious Wakeup⚠️ 现象线程没有被 notify/notifyAll 唤醒也没有被 interrupt却从 wait() 返回了 原因 - JVM/操作系统底层实现机制如 Linux 的 futex - 为了性能优化允许无理由唤醒 ✅ 应对方案 - 永远用 while 循环检查条件而非 if - 这是 Java 并发编程的铁律所有源码如 ArrayBlockingQueue都遵守 记忆口诀 等待用 while唤醒再检查虚假唤醒也不怕四、实战案例单缓冲区生产者消费者完整可运行 共享资源Box容量1/** * 单元素缓冲区生产-消费经典模型 * 状态0空1满 */classBox{privateintproduct0;// 0:空, 1:满privatefinalObjectlocknewObject();/** * 生产缓冲区空才能生产生产后通知消费者 */publicvoidproduce(intvalue)throwsInterruptedException{synchronized(lock){// 缓冲区满则等待while(product1){System.out.println( 缓冲区满生产者等待...);lock.wait();// 释放锁进入等待}// ✅ 执行生产product1;System.out.println( 生产者生产: value当前状态: 满);// 通知消费者可能正在等待lock.notifyAll();}}/** * 消费缓冲区满才能消费消费后通知生产者 */publicintconsume()throwsInterruptedException{synchronized(lock){// 缓冲区空则等待while(product0){System.out.println( 缓冲区空消费者等待...);lock.wait();}// ✅ 执行消费product0;System.out.println( 消费者消费当前状态: 空);// 通知生产者可能正在等待lock.notifyAll();return1;// 简化返回固定值}}} 线程启动 运行publicclassProducerConsumerDemo{publicstaticvoidmain(String[]args){BoxboxnewBox();// 生产者线程ThreadproducernewThread(()-{try{for(inti1;i5;i){box.produce(i);Thread.sleep(500);// 模拟生产耗时放锁外}}catch(InterruptedExceptione){Thread.currentThread().interrupt();System.out.println(生产者被中断);}},Producer-Thread);// 消费者线程ThreadconsumernewThread(()-{try{for(inti1;i5;i){box.consume();Thread.sleep(800);// 模拟消费耗时放锁外}}catch(InterruptedExceptione){Thread.currentThread().interrupt();System.out.println(消费者被中断);}},Consumer-Thread);// 启动线程producer.start();consumer.start();// ⏳ 主线程等待实际项目可用 CountDownLatchproducer.join();consumer.join();System.out.println(✅ 生产消费完成);}} 执行流程图解时间线: T0: Producer 启动检查 product0空→ 生产 product1 → notifyAll → sleep T1: Consumer 启动检查 product1满→ 消费 product0 → notifyAll → sleep T2: Producer 醒来检查 product0 → 生产 → notifyAll → sleep T3: Consumer 醒来检查 product1 → 消费 → notifyAll → sleep ... ✅ 完美交替无忙等待无超卖/空取 关键设计 1. while 循环检查条件 → 防虚假唤醒 2. wait() 释放锁 → 允许对方线程执行 3. notifyAll() 通知 → 避免唤醒错误线程 4. sleep() 放锁外 → 提升并发度五、进阶多生产者多消费者 有界缓冲区 升级版ArrayBuffer容量N/** * 有界缓冲区支持多生产者/多消费者 * 使用循环数组 头尾指针实现 */classArrayBufferT{privatefinalObject[]items;privateintcount;// 当前元素个数privateintputIndex;// 下一个放入位置privateinttakeIndex;// 下一个取出位置privatefinalObjectlocknewObject();publicArrayBuffer(intcapacity){itemsnewObject[capacity];}/** * 放入元素缓冲区满则等待 */SuppressWarnings(unchecked)publicvoidput(Titem)throwsInterruptedException{synchronized(lock){// 缓冲区满则等待while(countitems.length){System.out.println( 缓冲区满生产者等待...);lock.wait();}// ✅ 放入元素items[putIndex]item;putIndex(putIndex1)%items.length;// 循环指针count;System.out.println( 放入: item, 当前数量: count);// 通知消费者可能有多个在等待lock.notifyAll();}}/** * 取出元素缓冲区空则等待 */SuppressWarnings(unchecked)publicTtake()throwsInterruptedException{synchronized(lock){// 缓冲区空则等待while(count0){System.out.println( 缓冲区空消费者等待...);lock.wait();}// ✅ 取出元素Titem(T)items[takeIndex];items[takeIndex]null;// 帮助 GCtakeIndex(takeIndex1)%items.length;count--;System.out.println( 取出: item, 当前数量: count);// 通知生产者lock.notifyAll();returnitem;}}} 为什么这个实现是线程安全的互斥所有操作在synchronized(lock)内同一时间只有一个线程执行可见性wait/notify机制保证工作内存与主内存同步条件等待while循环确保满才能 put空才能 take通知唤醒notifyAll确保所有等待者有机会重新检查条件这就是 JDKArrayBlockingQueue的核心原理实际项目中建议直接使用BlockingQueue系列而非手写 wait/notify。六、 今日实战任务构建简易任务调度器任务1复现单缓冲区生产消费/** * 要求 * 1. 实现 Box 类容量1支持 produce/consume * 2. 1 个生产者线程生产 1~5每次间隔 500ms * 3. 1 个消费者线程消费并打印每次间隔 800ms * 4. 观察输出是否完美交替有无死锁/空取 * * 调试技巧 * - 为每个线程设置语义化名称 * - 在 wait/notify 处打印日志追踪线程状态 */任务2升级多生产者多消费者/** * 要求 * 1. 使用 ArrayBuffer容量3 * 2. 启动 2 个生产者分别生产 P1-1~P1-5, P2-1~P2-5 * 3. 启动 3 个消费者竞争消费 * 4. 观察是否出现生产者饥饿或消费者空转 * * 挑战 * - 如果消费者处理速度远慢于生产者缓冲区会怎样 * - 如何实现优雅停机生产完成后通知消费者退出 */任务3模拟 SpringBoot 异步日志实战高频/** * 异步日志写入器业务线程生产日志后台线程消费写入文件 * * 要求 * 1. LogBuffer有界缓冲区容量100存储日志字符串 * 2. LoggerProducer业务线程调用 log(msg) → 放入缓冲区满则阻塞 * 3. LoggerConsumer后台守护线程循环取日志 → 写入文件空则等待 * 4. 支持优雅关闭调用 shutdown() 后处理完剩余日志再退出 * * 最佳实践 * - 消费者设为守护线程setDaemon(true) * - shutdown 时设置标志位 notifyAll 唤醒消费者 * - 日志写入用 BufferedWriter 定期 flush */任务4对比 wait/notify vs BlockingQueue理解框架设计/** * 用两种方式实现相同的生产消费逻辑对比代码复杂度 * * 方式1手写 wait/notify今天学的 * 方式2使用 ArrayBlockingQueueJDK 原生 * * 要求 * 1. 分别实现两种版本 * 2. 对比代码量、可读性、异常处理、扩展性 * 3. 思考为什么框架推荐用 BlockingQueue * * 结论预告 * - BlockingQueue 封装了 wait/notifyAPI 更简洁 * - 内置超时、中断、批量操作等高级特性 * - 经过 JDK 团队充分测试更可靠 * ✅ 生产环境优先用 BlockingQueue理解原理即可 */ 第18天 · 核心总结极简背诵版线程通信三要素 共享资源 synchronized 锁 wait/notify 机制 ✅ 条件不满足 → wait() 释放锁等待 ✅ 条件改变后 → notifyAll() 唤醒等待者三大方法铁律方法作用关键特性注意事项wait()等待条件释放锁进入等待池必须 catch InterruptedExceptionnotify()唤醒一个随机选择不可控⚠️ 可能唤醒错误线程慎用notifyAll()唤醒全部所有等待者竞争锁✅ 生产环境推荐避免死锁经典范式模板直接复用synchronized(lock){while(条件不满足){// ❗ 必须 while防虚假唤醒lock.wait();// 释放锁等待通知}// ✅ 执行业务逻辑doSomething();// 状态改变通知他人lock.notifyAll();// ✅ 推荐 notifyAll}关键设计原则条件检查用 while虚假唤醒是真实存在的wait 释放锁让其他线程有机会修改条件优先 notifyAll避免唤醒错误线程导致的逻辑错误⏱️耗时操作放锁外sleep/IO/网络调用不要放在 synchronized 内生产环境建议✅ 理解 wait/notify 原理但优先使用java.util.concurrent工具类✅BlockingQueue/CountDownLatch/CyclicBarrier更安全可靠✅ 日志记录 wait/notify 事件便于排查线程不唤醒问题❌ 避免在锁内调用外部未知方法可能死锁

更多文章