ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java 阻塞队列——实现生产者消费者模型

Java 阻塞队列——实现生产者消费者模型 一、什么是生产者消费者模型生产者消费者Producer-Consumer是多线程里最经典的设计模型核心就是解耦、削峰、并行。 角色划分生产者负责产生数据/任务往队列里放消费者负责处理数据/任务从队列里取缓冲区队列生产者和消费者之间的中间容器作为“仓库”。生活化比喻包饺子。多人包饺子有的人负责擀皮生产者有的人负责包消费者擀好的饺子皮放在盘子缓冲区队列。盘子满了擀皮的暂停盘子空了包饺子的等待。模型三大核心作用解耦生产者和消费者不需要直接通信只依赖队列。生产者代码改动不会直接影响消费者。分布式系统中A服务生产消息放入队列B服务消费A、B互不感知。削峰流量缓冲突发大量请求时请求先放入队列消费者按自身能力慢慢处理不会瞬间压垮下游服务。举例服务A突增大量请求直接调用服务BB会被打崩中间加队列请求排队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 BlockingQueueString 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 区别高频考点特性ArrayListLinkedList底层结构动态 Object 数组内存连续双向链表内存分散随机访问 getO(1)O(n)尾部 addO (1)扩容时 O (n)O(1)中间插入 / 删除O (n)移动元素O (n)遍历找节点内存开销较小仅预留数组空间大每个节点存双指针遍历推荐普通 for / 迭代器迭代器 / 增强 for禁止 forget线程安全非线程安全非线程安全
返回列表