Java线程协作实战:wait/notify+Condition精准通信,吃透生产者-消费者模型(附面试避坑)
本文配套源码、多线程思维导图已整理至公众号「咖啡 Java 研习室」,回复【学习资料】即可领取;系列文章持续更新,关注不迷路~
目录
- 为什么线程通信是面试高频?(痛点直击)
- 方案1:synchronized+wait/notify(基础版协作)
- 方案2:Lock+Condition(精准通知,面试重点)
- 两种方案核心对比(表格速记)
- 实战:订单配送协作系统(集合+IO+异常整合)
- 面试避坑&拓展练习
一、为什么线程通信是面试高频?(痛点直击)
多线程开发中,「线程同步」解决“抢资源不打架”,而「线程通信」解决“分工合作不内耗”——这俩是面试必问的核心组合!
举个反例:
- 生产者疯狂生产,仓库满了还不停→内存浪费
- 消费者无脑循环消费,仓库空了还在查→CPU空转
- 最终系统效率低、资源浪费,甚至出现数据错乱
而线程通信的核心就是「按需工作」:
✅ 仓库满→生产者暂停,等消费者通知
✅ 仓库空→消费者等待,等生产者通知
✅ 状态变了→精准唤醒,不做无效操作
今天就拆解两种核心通信方案,从基础到实战,再到面试踩坑,一次性吃透!
二、方案1:synchronized+wait/notify(基础版协作)
wait()、notify()、notifyAll()是Java内置通信方法,必须配合synchronized锁使用,调用对象是锁对象本身。
1. 核心方法解析(面试常问)
| 方法 | 作用说明 | 注意点 |
|---|---|---|
| wait() | 释放锁+进入等待状态,需被notify()/notifyAll()唤醒或超时自动唤醒 | 必须处理InterruptedException;不能用if判断条件(下文避坑) |
| notify() | 随机唤醒锁对象上1个等待线程,唤醒后需重新竞争锁 | 多生产者多消费者场景易导致“永久等待”,不推荐单用 |
| notifyAll() | 唤醒锁对象上所有等待线程,避免漏唤醒 | 适合多角色协作,是生产环境首选 |
2. 实战:基础版生产者-消费者(集合+IO日志)
用ArrayList做仓库,实现“生产→存仓→消费”的基础协作,同时用IO记录商品流转日志(衔接Java IO知识点)。
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
// 商品实体
class Goods {
private String id;
private String name;
public Goods(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "商品【" + id + "】" + name;
}
public String getId() {
return id;
}
}
// 仓库类:synchronized+wait/notify核心逻辑
class GoodsWarehouse {
private static final int MAX_SIZE = 3; // 仓库最大容量
private List<Goods> warehouse = new ArrayList<>();
// 生产商品
public synchronized void produce(Goods goods) throws InterruptedException {
// 🔴 面试坑:必须用while循环(而非if)避免虚假唤醒
while (warehouse.size() == MAX_SIZE) {
System.out.println("仓库已满," + Thread.currentThread().getName() + " 进入等待");
this.wait(); // 释放锁,进入等待队列
}
// 生产逻辑
warehouse.add(goods);
System.out.println(Thread.currentThread().getName() + " 生产了" + goods + ",当前库存:" + warehouse.size());
writeLog("生产", goods); // 写入日志
this.notifyAll(); // 唤醒所有等待线程(生产者/消费者)
}
// 消费商品
public synchronized Goods consume() throws InterruptedException {
while (warehouse.size() == 0) {
System.out.println("仓库已空," + Thread.currentThread().getName() + " 进入等待");
this.wait();
}
// 消费逻辑
Goods goods = warehouse.remove(0);
System.out.println(Thread.currentThread().getName() + " 消费了" + goods + ",当前库存:" + warehouse.size());
writeLog("消费", goods);
this.notifyAll(); // 唤醒生产者继续生产
return goods;
}
// IO日志记录(衔接Java IO知识点)
private void writeLog(String type, Goods goods) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter("goods_log.txt", true))) {
String log = Thread.currentThread().getName() + "|" + type + "|" + goods.getId() + "|" + System.currentTimeMillis() + "\n";
bw.write(log);
} catch (IOException e) {
System.out.println("日志写入失败");
e.printStackTrace();
}
}
}
// 生产者任务
class Producer implements Runnable {
private GoodsWarehouse warehouse;
private int producerId;
public Producer(GoodsWarehouse warehouse, int producerId) {
this.warehouse = warehouse;
this.producerId = producerId;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
Goods goods = new Goods("P" + producerId + "-" + i, "零食" + i);
warehouse.produce(goods);
Thread.sleep(500); // 模拟生产耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费者任务
class Consumer implements Runnable {
private GoodsWarehouse warehouse;
public Consumer(GoodsWarehouse warehouse) {
this.warehouse = warehouse;
}
@Override
public void run() {
for (int i = 1; i <= 8; i++) {
try {
warehouse.consume();
Thread.sleep(800); // 模拟消费耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 测试类
public class WaitNotifyDemo {
public static void main(String[] args) {
GoodsWarehouse warehouse = new GoodsWarehouse();
// 2个生产者+2个消费者
new Thread(new Producer(warehouse, 1), "生产者A").start();
new Thread(new Producer(warehouse, 2), "生产者B").start();
new Thread(new Consumer(warehouse), "消费者X").start();
new Thread(new Consumer(warehouse), "消费者Y").start();
}
}
3. 面试避坑:为什么用while而非if判断等待条件?
👉 虚假唤醒问题:线程被notify()唤醒后,可能仓库状态已变(比如其他线程抢先生产/消费),若用if判断,唤醒后会直接执行后续代码,导致库存超标或空仓消费。
而while循环会在唤醒后重新校验条件,确保符合状态才继续执行——这是面试高频考点,记死!
三、方案2:Lock+Condition(精准通知,面试重点)
wait/notify的短板是「随机唤醒」:比如生产者唤醒生产者、消费者唤醒消费者,导致无效唤醒,效率低下。
而Lock接口搭配Condition类,能实现「定向精准通知」:生产者只唤醒消费者,消费者只唤醒生产者,零无效操作!
1. 核心逻辑
- 创建Lock锁对象(推荐ReentrantLock);
- 为不同角色创建专属Condition队列(生产者队列+消费者队列);
- 状态不满足时,调用对应Condition的await()进入等待;
- 状态满足后,调用目标角色的signal()/signalAll()精准唤醒。
2. 实战:进阶版多生产者多消费者
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
// 复用Goods类(同上,核心逻辑不变)
class AdvancedWarehouse {
private static final int MAX_CAP = 4; // 仓库最大容量
private List<Goods> goodsList = new ArrayList<>();
private ReentrantLock lock = new ReentrantLock(); // Lock锁
private Condition producerCond = lock.newCondition(); // 生产者等待队列
private Condition consumerCond = lock.newCondition(); // 消费者等待队列
// 生产商品
public void produce(Goods goods) throws InterruptedException {
lock.lock(); // 加锁
try {
while (goodsList.size() == MAX_CAP) {
System.out.println("仓库满," + Thread.currentThread().getName() + " 等待");
producerCond.await(); // 生产者进入专属队列等待
}
goodsList.add(goods);
System.out.println(Thread.currentThread().getName() + " 生产" + goods + ",库存:" + goodsList.size());
consumerCond.signalAll(); // 只唤醒消费者队列
} finally {
lock.unlock(); // 必须在finally释放锁,避免死锁
}
}
// 消费商品
public Goods consume() throws InterruptedException {
lock.lock();
try {
while (goodsList.size() == 0) {
System.out.println("仓库空," + Thread.currentThread().getName() + " 等待");
consumerCond.await(); // 消费者进入专属队列等待
}
Goods goods = goodsList.remove(0);
System.out.println(Thread.currentThread().getName() + " 消费" + goods + ",库存:" + goodsList.size());
producerCond.signalAll(); // 只唤醒生产者队列
return goods;
} finally {
lock.unlock();
}
}
}
// 测试类
public class ConditionDemo {
public static void main(String[] args) {
AdvancedWarehouse warehouse = new AdvancedWarehouse();
// 3个生产者+2个消费者(多角色场景更能体现精准通知优势)
for (int i = 1; i <= 3; i++) {
int finalI = i;
new Thread(() -> {
for (int j = 1; j <= 4; j++) {
try {
Goods goods = new Goods("CP" + finalI + "-" + j, "日用品" + j);
warehouse.produce(goods);
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "高级生产者" + i).start();
}
for (int i = 1; i <= 2; i++) {
new Thread(() -> {
for (int j = 1; j <= 6; j++) {
try {
warehouse.consume();
Thread.sleep(600);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "高级消费者" + i).start();
}
}
}
3. 核心优势
- 精准唤醒:避免无效竞争,效率比wait/notify高30%+;
- 灵活控制:可创建多个Condition队列,支持多角色协作(比如“厨师→服务员→顾客”三级协作);
- 锁粒度更细:Lock支持可中断锁、超时锁,比synchronized更灵活(后续文章详解)。
四、两种通信方案核心对比(面试速记)
| 对比维度 | wait/notify | Condition |
|---|---|---|
| 核心依赖 | synchronized关键字 | Lock接口(ReentrantLock) |
| 唤醒方式 | 随机唤醒/全部唤醒 | 定向精准唤醒(支持多队列) |
| 灵活性 | 低(仅1个等待队列) | 高(多个Condition队列) |
| 异常处理 | 必须处理InterruptedException | 必须处理InterruptedException |
| 适用场景 | 单生产者单消费者、简单协作 | 多生产者多消费者、复杂协作 |
| 面试出现频率 | ★★★☆☆ | ★★★★★(企业级开发首选) |
五、实战整合:订单配送协作系统(集合+IO+异常)
结合前文知识,开发一个贴近实际场景的「商家→配送员」协作系统,包含:
- Lock+Condition精准通信;
- ArrayList存储待配送订单;
- IO流记录配送日志;
- 完整异常处理(保障系统稳定性)。
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
// 订单实体
class Order {
private String orderNo;
private String goods;
private double price;
public Order(String orderNo, String goods, double price) {
this.orderNo = orderNo;
this.goods = goods;
this.price = price;
}
@Override
public String toString() {
return "订单【" + orderNo + "】-" + goods + "(¥" + price + ")";
}
public String getOrderNo() {
return orderNo;
}
}
// 订单配送中心(核心协作逻辑)
class OrderDeliveryCenter {
private static final int MAX_ORDER = 5; // 最大待配送订单数
private List<Order> orderQueue = new ArrayList<>();
private ReentrantLock lock = new ReentrantLock();
private Condition merchantCond = lock.newCondition(); // 商家等待队列
private Condition courierCond = lock.newCondition(); // 配送员等待队列
private PrintWriter logWriter;
// 初始化日志流(IO知识点)
public OrderDeliveryCenter() {
try {
logWriter = new PrintWriter(new FileWriter("delivery_log.txt", true));
} catch (IOException e) {
System.out.println("日志初始化失败");
e.printStackTrace();
}
}
// 商家提交订单(生产)
public void submitOrder(Order order) throws InterruptedException {
lock.lock();
try {
while (orderQueue.size() == MAX_ORDER) {
System.out.println("待配送队列已满," + Thread.currentThread().getName() + " 等待");
merchantCond.await();
}
orderQueue.add(order);
System.out.println(Thread.currentThread().getName() + " 提交" + order + ",待配送数:" + orderQueue.size());
log("提交订单", order);
courierCond.signal(); // 唤醒1个配送员
} finally {
lock.unlock();
}
}
// 配送员取单配送(消费)
public void deliverOrder() throws InterruptedException {
lock.lock();
try {
while (orderQueue.size() == 0) {
System.out.println("无待配送订单," + Thread.currentThread().getName() + " 等待");
courierCond.await();
}
Order order = orderQueue.remove(0);
System.out.println(Thread.currentThread().getName() + " 取走" + order + "开始配送,剩余待配送:" + orderQueue.size());
log("开始配送", order);
merchantCond.signal(); // 唤醒1个商家
} finally {
lock.unlock();
}
}
// 日志记录
private void log(String type, Order order) {
if (logWriter != null) {
String log = System.currentTimeMillis() + "|" + Thread.currentThread().getName() + "|" + type + "|" + order.getOrderNo() + "\n";
logWriter.write(log);
logWriter.flush();
}
}
// 关闭日志流(资源释放)
public void closeLog() {
if (logWriter != null) {
logWriter.close();
}
}
}
// 商家任务
class MerchantTask implements Runnable {
private OrderDeliveryCenter center;
private String merchantName;
public MerchantTask(OrderDeliveryCenter center, String merchantName) {
this.center = center;
this.merchantName = merchantName;
}
@Override
public void run() {
for (int i = 1; i <= 6; i++) {
try {
Order order = new Order("ORDER-" + merchantName + "-" + i, "生鲜套餐" + i, 88.0 + i * 10);
center.submitOrder(order);
Thread.sleep(600); // 模拟接单耗时
} catch (InterruptedException e) {
System.out.println(merchantName + "提交订单被中断");
e.printStackTrace();
}
}
}
}
// 配送员任务
class CourierTask implements Runnable {
private OrderDeliveryCenter center;
private String courierId;
public CourierTask(OrderDeliveryCenter center, String courierId) {
this.center = center;
this.courierId = courierId;
}
@Override
public void run() {
for (int i = 1; i <= 9; i++) {
try {
center.deliverOrder();
Thread.sleep(800); // 模拟配送耗时
} catch (InterruptedException e) {
System.out.println(courierId + "配送被中断");
e.printStackTrace();
}
}
}
}
// 测试类
public class OrderDeliverySystem {
public static void main(String[] args) throws InterruptedException {
OrderDeliveryCenter center = new OrderDeliveryCenter();
// 2个商家+3个配送员
new Thread(new MerchantTask(center, "水果超市"), "商家A").start();
new Thread(new MerchantTask(center, "生鲜店"), "商家B").start();
new Thread(new CourierTask(center, "C001"), "配送员1").start();
new Thread(new CourierTask(center, "C002"), "配送员2").start();
new Thread(new CourierTask(center, "C003"), "配送员3").start();
// 等待任务完成,关闭日志流
Thread.sleep(10000);
center.closeLog();
System.out.println("===== 订单配送任务结束 =====");
}
}
执行效果
- 商家提交订单时,若队列满则暂停,配送员取单后自动唤醒;
- 配送员无单时等待,商家提交后精准唤醒,无无效竞争;
- 所有操作记录到
delivery_log.txt,方便问题排查。
六、面试避坑&拓展练习
1. 面试高频问题(记牢直接加分)
- Q:wait()和sleep()的区别?
A:① wait()释放锁,sleep()不释放;② wait()需配合synchronized,sleep()可单独使用;③ wait()唤醒后需重新竞争锁,sleep()时间到直接继续。
- Q:Condition的signal()和notify()的区别?
A:signal()唤醒指定Condition队列的线程,notify()随机唤醒锁对象上的线程;Condition支持多队列,notify()仅支持单队列。
- Q:多生产者多消费者场景,用wait/notify会有什么问题?
A:无效唤醒(生产者唤醒生产者)、永久等待(部分线程未被唤醒),推荐用Condition精准通知。
2. 拓展练习(强化实战)
尝试用Condition实现「厨师做菜→服务员传菜→顾客用餐」的三级协作,要求:
- 厨师做完菜通知服务员,服务员传完通知顾客;
- 用IO记录每一步操作日志;
- 避免虚假唤醒和无效唤醒。
👉 完整实现思路和源码,在公众号「咖啡 Java 研习室」回复【学习资料】即可获取!
七、明日预告&资源领取
今天我们掌握了线程通信的两种核心方案,解决了“多线程协作内耗”问题。明天将学习企业级开发必备技能——线程池,吃透核心参数、工作原理和实战应用,把多线程知识整合到最终项目中!
✅ 本文配套源码、多线程思维导图、面试题合集,已整理至公众号「咖啡Java研习室」,回复【学习资料】即可领取;
✅ 系列文章持续更新,关注公众号获取更多Java核心知识点、实战案例和面试干货,一起进阶Java工程师~
#Java多线程 #生产者消费者模型 #waitnotify #Condition #Java面试