Java 阻塞队列——实现生产者消费者模型
一、什么是生产者消费者模型
生产者消费者(Producer-Consumer)是多线程里最经典的设计模型,核心就是解耦、削峰、并行。 角色划分:
- 生产者:负责产生数据/任务,往队列里放;
- 消费者:负责处理数据/任务,从队列里取;
- 缓冲区(队列):生产者和消费者之间的中间容器,作为“仓库”。
生活化比喻:包饺子。多人包饺子,有的人负责擀皮(生产者),有的人负责包(消费者),擀好的饺子皮放在盘子(缓冲区队列)。盘子满了,擀皮的暂停;盘子空了,包饺子的等待。
模型三大核心作用
- 解耦:生产者和消费者不需要直接通信,只依赖队列。生产者代码改动,不会直接影响消费者。分布式系统中,A服务生产消息放入队列,B服务消费,A、B互不感知。
- 削峰(流量缓冲):突发大量请求时,请求先放入队列,消费者按自身能力慢慢处理,不会瞬间压垮下游服务。
举例:服务A突增大量请求,直接调用服务B,B会被打崩;中间加队列,请求排队,B匀速消费,抵御流量尖刺。
- 并行处理:生产、消费两个动作可以并发执行,提升整体吞吐量。生产者只管生产,消费者只管消费,互不阻塞。
代价
引入队列后带来额外开销:队列占用内存;多线程读写队列需要加锁,带来锁竞争;还要处理队列满、队列空、并发异常等问题,代码复杂度上升。
需要部署更多的设备,生产环境也会更加复杂,管理起来更麻烦
二、Java中实现:阻塞队列 BlockingQueue
生产者消费者模型,最推荐使用BlockingQueue阻塞队列,JDK原生提供,不用手写wait/notify,线程安全。
BlockingQueue特点:
- 队列满时,生产者调用
put()放入元素,线程自动阻塞等待;- 队列空时,消费者调用
take()取出元素,线程自动阻塞等待;
常用实现类:
ArrayBlockingQueue:数组实现,有界队列,初始化必须指定容量,底层是数组。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()
四、常见坑点总结(面试高频)
- 无界队列OOM风险:LinkedBlockingQueue不设置容量,任务无限堆积,内存耗尽。生产环境尽量用有界队列。
- 虚假唤醒:wait判断条件必须while,不能if。
- notify 信号丢失:推荐notifyAll,避免线程永久等待。
- 消费速度跟不上生产:队列持续积压,需要扩容消费者、优化消费逻辑、限流保护。
- 中断异常处理:BlockingQueue的put/take会抛出InterruptedException,捕获后要恢复中断标记
Thread.currentThread().interrupt(),不要吞掉中断。
五、Java ArrayList 与 LinkedList 区别(高频考点)
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态 Object 数组,内存连续 | 双向链表,内存分散 |
| 随机访问 get | O(1) | O(n) |
| 尾部 add | O (1),扩容时 O (n) | O(1) |
| 中间插入 / 删除 | O (n)(移动元素) | O (n)(遍历找节点) |
| 内存开销 | 较小,仅预留数组空间 | 大,每个节点存双指针 |
| 遍历推荐 | 普通 for / 迭代器 | 迭代器 / 增强 for,禁止 for+get |
| 线程安全 | 非线程安全 | 非线程安全 |
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)