一、什么是生产者消费者模型

生产者消费者(Producer-Consumer)是多线程里最经典的设计模型,核心就是解耦、削峰、并行。 角色划分:

  • 生产者:负责产生数据/任务,往队列里放;
  • 消费者:负责处理数据/任务,从队列里取;
  • 缓冲区(队列):生产者和消费者之间的中间容器,作为“仓库”。

生活化比喻:包饺子。多人包饺子,有的人负责擀皮(生产者),有的人负责包(消费者),擀好的饺子皮放在盘子(缓冲区队列)。盘子满了,擀皮的暂停;盘子空了,包饺子的等待。

模型三大核心作用

  1. 解耦:生产者和消费者不需要直接通信,只依赖队列。生产者代码改动,不会直接影响消费者。分布式系统中,A服务生产消息放入队列,B服务消费,A、B互不感知。
  2. 削峰(流量缓冲):突发大量请求时,请求先放入队列,消费者按自身能力慢慢处理,不会瞬间压垮下游服务。

    举例:服务A突增大量请求,直接调用服务B,B会被打崩;中间加队列,请求排队,B匀速消费,抵御流量尖刺。

  3. 并行处理:生产、消费两个动作可以并发执行,提升整体吞吐量。生产者只管生产,消费者只管消费,互不阻塞。

代价

引入队列后带来额外开销:队列占用内存;多线程读写队列需要加锁,带来锁竞争;还要处理队列满、队列空、并发异常等问题,代码复杂度上升。

需要部署更多的设备,生产环境也会更加复杂,管理起来更麻烦

二、Java中实现:阻塞队列 BlockingQueue

生产者消费者模型,最推荐使用BlockingQueue阻塞队列,JDK原生提供,不用手写wait/notify,线程安全。

BlockingQueue特点:

  • 队列满时,生产者调用put()放入元素,线程自动阻塞等待;
  • 队列空时,消费者调用take()取出元素,线程自动阻塞等待;

常用实现类:

  1. ArrayBlockingQueue:数组实现,有界队列,初始化必须指定容量,底层是数组。
  2. LinkedBlockingQueue:链表实现,可设置有界/无界。无界时风险极大:生产速度持续大于消费,元素无限堆积,内存暴涨,触发OOM。

重点:不要随便使用无界LinkedBlockingQueue,生产环境很容易内存溢出。

基础代码示例(BlockingQueue版本)

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class ProducerConsumerDemo {
    public static void main(String[] args) {
        // 缓冲区,容量设为5
        BlockingQueue<String> queue = new LinkedBlockingQueue<>(5);

        // 生产者线程
        Thread producer = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    String data = "任务-" + i;
                    queue.put(data); // 队列满,自动阻塞
                    System.out.println("生产者生产:" + data);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }, "生产者");

        // 消费者线程
        Thread consumer = new Thread(() -> {
            while (true) {
                try {
                    String task = queue.take(); // 队列空,自动阻塞
                    System.out.println("消费者处理:" + task);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        }, "消费者");

        producer.start();
        consumer.start();
    }
}

三、底层原理:wait + notify 手写版本 & 虚假唤醒

我们也可以不用BlockingQueue,使用synchronized + wait() + notify()手写缓冲区。这里会遇到面试高频考点:虚假唤醒。

什么是虚假唤醒

wait() 的线程,在没有被notify唤醒的情况下,也可能被操作系统唤醒。 所以不能用if判断队列状态,必须用while循环。

错误写法:

// ❌错误!if只判断一次,虚假唤醒后不会二次校验
if(队列是空){
    wait();
}

正确写法:

// ✅while循环,唤醒之后再次循环检查条件
while(队列是空){
    wait();
}

原理:线程被唤醒后,会重新回到while条件判断。如果此时队列依旧为空,会再次进入wait等待,规避虚假唤醒带来的bug。

wait/notify 手写简易生产者消费者

class MyBlockingQueue {
    private String[] data = null;

    // 队首
    private int head = 0;

    // 队尾
    private int tail = 0;

    // 元素个数
    private int size = 0;


    public MyBlockingQueue(int capacity) {
        data = new String[capacity];
    }

    public void put(String elem) throws InterruptedException {
        synchronized (this) {
            while (size >= data.length) {
                // 队列满了. 需要阻塞的
                // return;
                this.wait();
            }
            data[tail] = elem;
            tail++;
            if (tail >= data.length) {
                tail = 0;
            }

            size++;
            this.notify();
        }
    }

    public String take() throws InterruptedException {
        synchronized (this) {
            while (size == 0) {
                // 队列空了. 需要阻塞
                // return null;
                this.wait();
            }
            String ret = data[head];
            head++;
            if (head >= data.length) {
                head = 0;
            }
            size--;
            this.notify();
            return ret;
        }
    }
}

public class Demo31 {
    public static void main(String[] args) {
        MyBlockingQueue queue = new MyBlockingQueue(1000);

        Thread producer = new Thread(() -> {
            int n = 0;
            while (true) {
                try {
                    queue.put(n + "");
                    System.out.println("生产元素 " + n);
                    n++;
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        Thread consumer = new Thread(() -> {
            while (true) {
                String n = null;
                try {
                    n = queue.take();
                    System.out.println("消费元素 " + n);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        producer.start();
        consumer.start();
    }
}

注意:优先使用notifyAll()而不是notify()。notify只随机唤醒一个线程,容易出现信号丢失,所有线程全部阻塞,发生死锁。

这里就两个线程互相唤醒,所以使用notify()

四、常见坑点总结(面试高频)

  1. 无界队列OOM风险:LinkedBlockingQueue不设置容量,任务无限堆积,内存耗尽。生产环境尽量用有界队列。
  2. 虚假唤醒:wait判断条件必须while,不能if。
  3. notify 信号丢失:推荐notifyAll,避免线程永久等待。
  4. 消费速度跟不上生产:队列持续积压,需要扩容消费者、优化消费逻辑、限流保护。
  5. 中断异常处理:BlockingQueue的put/take会抛出InterruptedException,捕获后要恢复中断标记Thread.currentThread().interrupt(),不要吞掉中断。

五、Java ArrayList 与 LinkedList 区别(高频考点)

特性ArrayListLinkedList
底层结构动态 Object 数组,内存连续双向链表,内存分散
随机访问 getO(1)O(n)
尾部 addO (1),扩容时 O (n)O(1)
中间插入 / 删除O (n)(移动元素)O (n)(遍历找节点)
内存开销较小,仅预留数组空间大,每个节点存双指针
遍历推荐普通 for / 迭代器迭代器 / 增强 for,禁止 for+get
线程安全非线程安全非线程安全
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐