你能否用一个生产者-消费者案例展示线程间的协作(wait/notify)

wen java案例 55

生产者-消费者模式下的wait/notify深度解析

📖 目录导读

  1. 引言:多线程协作的痛点
  2. 生产者-消费者模式概述
  3. wait/notify机制的核心原理
  4. 完整代码案例与逐行解析
  5. 常见陷阱与性能优化
  6. 问答环节:高频面试题与实战疑点
  7. 总结与最佳实践

你能否用一个生产者-消费者案例展示线程间的协作(wait/notify)

多线程协作的痛点

在现代软件开发中,多线程编程是提升性能的利器,但线程间的协调问题常常让开发者陷入困境,想象一个场景:一个线程不断生产数据,另一个线程需要及时消费这些数据,如果生产速度过快,数据会堆积;消费速度过快,则会出现空等待,如何让两个线程“心有灵犀”地协作?

经典问题:两个线程如何共享一个有限容量的缓冲区,既不丢失数据,也不重复消费?这正是生产者-消费者问题的核心。


生产者-消费者模式概述

生产者-消费者模式是一个经典并发设计模式,它通过一个共享缓冲区解耦生产者和消费者。

  • 生产者:负责生成数据并放入缓冲区
  • 消费者:负责从缓冲区取出数据进行处理
  • 缓冲区:通常是有界队列,用于临时存储和流量控制

模式的价值

  1. 解耦:生产者和消费者不直接依赖
  2. 异步:生产与消费可不同步进行
  3. 削峰填谷:缓冲器能平滑数据流

问题:如何确保缓冲区满时生产者等待,缓冲区空时消费者等待?这正是wait/notify的用武之地。


wait/notify机制的核心原理

Java中的wait()notify()notifyAll()是Object类提供的线程通信方法,它们必须在同步块(synchronized)内使用。

工作原理

  • wait():当前线程释放锁并进入等待状态,直到其他线程调用notify/notifyAll
  • notify():随机唤醒一个在同一个锁对象上等待的线程
  • notifyAll():唤醒所有在同一个锁对象上等待的线程

关键规则

  1. 调用wait/notify前必须持有对象的monitor锁
  2. wait会释放锁,notify不会释放锁
  3. 被唤醒的线程需要重新竞争锁

完整代码案例与逐行解析

下面是一个基于Java的生产者-消费者案例,使用wait/notify实现线程协作。

import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumerExample {
    // 共享缓冲区:最大容量为5
    private static final int CAPACITY = 5;
    private final Queue<Integer> buffer = new LinkedList<>();
    private final Object lock = new Object();
    // 生产者
    class Producer implements Runnable {
        @Override
        public void run() {
            int value = 0;
            while (true) {
                synchronized (lock) {
                    // 缓冲区满时等待
                    while (buffer.size() == CAPACITY) {
                        System.out.println("缓冲区已满,生产者等待...");
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                            return;
                        }
                    }
                    // 生产数据
                    buffer.offer(value);
                    System.out.println("生产者生产: " + value + " 当前缓冲区: " + buffer.size());
                    value++;
                    // 通知消费者消费
                    lock.notifyAll();
                }
                // 模拟生产耗时
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return;
                }
            }
        }
    }
    // 消费者
    class Consumer implements Runnable {
        @Override
        public void run() {
            while (true) {
                synchronized (lock) {
                    // 缓冲区空时等待
                    while (buffer.isEmpty()) {
                        System.out.println("缓冲区为空,消费者等待...");
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                            return;
                        }
                    }
                    // 消费数据
                    int value = buffer.poll();
                    System.out.println("消费者消费: " + value + " 当前缓冲区: " + buffer.size());
                    // 通知生产者生产
                    lock.notifyAll();
                }
                // 模拟消费耗时
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return;
                }
            }
        }
    }
    public static void main(String[] args) {
        ProducerConsumerExample example = new ProducerConsumerExample();
        Thread producerThread = new Thread(example.new Producer(), "生产者线程");
        Thread consumerThread = new Thread(example.new Consumer(), "消费者线程");
        producerThread.start();
        consumerThread.start();
    }
}

关键代码解读

  1. wait放在while循环内:防止“虚假唤醒”(spurious wakeup),确保条件满足后继续执行
  2. 使用notifyAll而非notify:避免“信号丢失”,因为notify可能唤醒同类型线程导致死锁
  3. 共享锁对象:生产者和消费者使用同一个lock对象,确保互斥和协作

运行结果示例

生产者生产: 0 当前缓冲区: 1
消费者消费: 0 当前缓冲区: 0
缓冲区为空,消费者等待...
生产者生产: 1 当前缓冲区: 1
生产者生产: 2 当前缓冲区: 2
...
缓冲区已满,生产者等待...
消费者消费: 4 当前缓冲区: 4

常见陷阱与性能优化

常见陷阱

  1. 死锁:忘记在wait前释放锁或使用错误锁对象
  2. 信号丢失:notify时没有检查条件,导致等待线程永远不被唤醒
  3. 嵌套同步:在同步块内调用其他同步方法可能导致死锁

性能优化技巧

  1. 使用ReentrantLock与Condition:提供更灵活的等待/通知机制,支持公平锁和多个条件队列
  2. 选择合适的数据结构LinkedBlockingQueueArrayBlockingQueue内置了阻塞机制,可替代手写wait/notify
  3. 避免频繁notifyAll:大量线程竞争锁时,notifyAll会引发“惊群效应”,降低性能

优化后的代码(使用BlockingQueue)

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class OptimizedProducerConsumer {
    private static final int CAPACITY = 5;
    private final BlockingQueue<Integer> buffer = new ArrayBlockingQueue<>(CAPACITY);
    class Producer implements Runnable {
        @Override
        public void run() {
            int value = 0;
            try {
                while (true) {
                    buffer.put(value);
                    System.out.println("生产者生产: " + value++);
                    Thread.sleep(500);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    class Consumer implements Runnable {
        @Override
        public void run() {
            try {
                while (true) {
                    int value = buffer.take();
                    System.out.println("消费者消费: " + value);
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    public static void main(String[] args) {
        OptimizedProducerConsumer example = new OptimizedProducerConsumer();
        new Thread(example.new Producer()).start();
        new Thread(example.new Consumer()).start();
    }
}

优势:BlockingQueue内置了lock和condition,无需手动管理wait/notify,代码更简洁、更安全。


问答环节:高频面试题与实战疑点

Q1: wait()和sleep()有什么区别?

A

  • 所属类:wait是Object方法,sleep是Thread静态方法
  • 锁释放:wait释放锁,sleep不释放锁
  • 唤醒方式:wait需要notify/notifyAll唤醒,sleep到时间自动唤醒
  • 使用场景:wait用于线程通信,sleep用于暂停执行

Q2: 为什么wait/notify必须在synchronized块内使用?

A:为了确保线程安全,如果不加锁,在检查条件(如buffer.isEmpty())和调用wait之间,其他线程可能修改状态,导致条件判断失效,synchronized保证原子性和可见性。

Q3: 为什么用while循环检查条件,而不是if?

A:防止虚假唤醒信号丢失,Java文档允许线程在没有notify的情况下被唤醒,while循环确保条件重新检查,避免“过早唤醒”导致的错误。

Q4: 多个生产者和消费者应该用notify还是notifyAll?

A:建议使用notifyAll,notify可能唤醒同类型线程(如两个生产者),导致另一个类型线程永远不被唤醒(信号丢失),notifyAll唤醒所有等待线程,确保系统正确运行,但可能引发轻微的性能开销。

Q5: wait/notify和Lock+Condition如何选择?

A

  • 简单场景:wait/notify足够,但需小心陷阱
  • 复杂场景:Lock+Condition更灵活,支持公平锁、多个条件队列、超时等待
  • 生产环境:优先使用BlockingQueue实现,避免手写线程通信

总结与最佳实践

生产者-消费者模式通过wait/notify实现了线程间的优雅协作,关键要点包括:

  1. 始终在同步块内使用wait/notify
  2. 使用while循环检查条件,防止虚假唤醒
  3. 优先使用notifyAll,避免信号丢失
  4. 考虑使用BlockingQueue,简化线程协调
  5. 注意中断处理,调用wait后需处理InterruptedException

最佳实践速查表

场景 推荐方案
一对生产者-消费者 wait/notify或BlockingQueue
多对多 Condition或BlockingQueue
高并发 Lock+公平锁+Condition
快速原型 BlockingQueue

线程协作的艺术在于平衡并发与安全,掌握wait/notify是理解高级并发工具的基础,但生产环境中应优先选择经过验证的并发集合类,一个好的生产者-消费者实现应该像一场完美的交响乐:每个线程各司其职,在合适的时机演奏(工作),在无声处等待(等待),共同奏出高效协调的乐章。

抱歉,评论功能暂时关闭!