Java线程协作实战:wait/notify+Condition精准通信,吃透生产者-消费者模型(附面试避坑)

22 阅读10分钟

Java线程协作实战:wait/notify+Condition精准通信,吃透生产者-消费者模型(附面试避坑)

本文配套源码、多线程思维导图已整理至公众号「咖啡 Java 研习室」,回复【学习资料】即可领取;系列文章持续更新,关注不迷路~

目录

  1. 为什么线程通信是面试高频?(痛点直击)
  2. 方案1:synchronized+wait/notify(基础版协作)
  3. 方案2:Lock+Condition(精准通知,面试重点)
  4. 两种方案核心对比(表格速记)
  5. 实战:订单配送协作系统(集合+IO+异常整合)
  6. 面试避坑&拓展练习

一、为什么线程通信是面试高频?(痛点直击)

多线程开发中,「线程同步」解决“抢资源不打架”,而「线程通信」解决“分工合作不内耗”——这俩是面试必问的核心组合!

举个反例:

  • 生产者疯狂生产,仓库满了还不停→内存浪费
  • 消费者无脑循环消费,仓库空了还在查→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. 核心逻辑

  1. 创建Lock锁对象(推荐ReentrantLock);
  2. 为不同角色创建专属Condition队列(生产者队列+消费者队列);
  3. 状态不满足时,调用对应Condition的await()进入等待;
  4. 状态满足后,调用目标角色的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/notifyCondition
核心依赖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实现「厨师做菜→服务员传菜→顾客用餐」的三级协作,要求:

  1. 厨师做完菜通知服务员,服务员传完通知顾客;
  2. 用IO记录每一步操作日志;
  3. 避免虚假唤醒和无效唤醒。

👉 完整实现思路和源码,在公众号「咖啡 Java 研习室」回复【学习资料】即可获取!

七、明日预告&资源领取

今天我们掌握了线程通信的两种核心方案,解决了“多线程协作内耗”问题。明天将学习企业级开发必备技能——线程池,吃透核心参数、工作原理和实战应用,把多线程知识整合到最终项目中!

✅ 本文配套源码、多线程思维导图、面试题合集,已整理至公众号「咖啡Java研习室」,回复【学习资料】即可领取;

✅ 系列文章持续更新,关注公众号获取更多Java核心知识点、实战案例和面试干货,一起进阶Java工程师~

#Java多线程 #生产者消费者模型 #waitnotify #Condition #Java面试