《Java 100 天进阶之路》第50篇:阻塞队列与并发容器(2026版)
第50篇:阻塞队列与并发容器(2026版)
📌系列导航:《Java 100 天进阶之路》完整目录 |
⬅️ 上一篇:第49篇:ConcurrentHashMap原理 |
➡️ 下一篇:第51篇:线程生命周期与创建方式
文章目录
- 第50篇:阻塞队列与并发容器(2026版)
- 🗺️ 本文阅读地图(3 分钟速览)
- 一、核心知识点
- 二、生活类比:从“奶茶店柜台”到“智能餐厅”
- 三、阻塞队列核心机制
- 3.1 什么是 BlockingQueue?
- 3.2 核心 API 速记
- 3.3 五大实现类对比
- 3.4 源码片段:ArrayBlockingQueue 的 put/take(锁+条件)
- 3.5 源码片段:LinkedBlockingQueue 的 put/take(双锁设计)
- 四、CopyOnWriteArrayList:读多写少的并发 List
- 4.1 核心思想
- 4.2 源码片段
- 4.3 优缺点与适用场景
- 五、ConcurrentLinkedQueue:CAS 无锁非阻塞队列
- 5.1 核心思想
- 5.2 核心特点
- 5.3 核心方法
- 六、线程池中的阻塞队列选型
- 七、生产级避坑清单
- 八、面试高频考点
- 面试官追问陷阱(加分题)
- 九、练习题
- 📊 你的学习进度
- 👉 下一篇文章预告
🗺️ 本文阅读地图(3 分钟速览)
第45~49篇拿下了 HashMap 和 ConcurrentHashMap,本篇是集合框架源码系列收官之作,聚焦并发场景下的队列与 List 容器:
| 模块 | 核心问题 | 一句话回答 |
|---|---|---|
| BlockingQueue 是什么 | 阻塞队列解决了什么问题? | 生产者-消费者模型的线程安全桥梁,队列满/空时自动阻塞 |
| Array vs Linked vs Sync | 三种阻塞队列怎么选? | 固定容量用 Array,高吞吐用 Linked,线程池传引用用 Sync |
| CopyOnWriteArrayList | 并发读多写少用什么 List? | 读无锁、写时复制,读多写少场景的王者 |
| ConcurrentLinkedQueue | 非阻塞队列是什么? | CAS 无锁实现,适合高并发、不要求阻塞的场景 |
| 线程池中的应用 | 阻塞队列在线程池里起什么作用? | 任务缓冲,核心参数之一 |
一、核心知识点
阻塞队列(BlockingQueue):
- 定义:支持阻塞入队/出队的线程安全队列,队列满时
put()阻塞生产者,队列空时take()阻塞消费者 - 五大实现:
ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue、DelayQueue - 核心 API:
put(e)/take()(阻塞)、offer(e)/poll()(非阻塞)、offer(e, timeout)/poll(timeout)(超时)
并发容器:
- CopyOnWriteArrayList:写时复制,读无锁写加锁,读多写少场景首选
- ConcurrentLinkedQueue:CAS 无锁非阻塞队列,高并发任务队列首选
二、生活类比:从“奶茶店柜台”到“智能餐厅”
BlockingQueue 就像一个奶茶店柜台:
- 柜台容量有限(有界队列),做好的奶茶放在柜台上。
- 顾客(消费者)来取,柜台空了就等着(
take()阻塞)。 - 店员(生产者)做好了放上去,柜台满了就等一下再放(
put()阻塞)。
SynchronousQueue 是“手递手”柜台:柜台没有放置空间,店员做好一杯奶茶,必须直接递到顾客手里,两边同时在场才能完成交接。
CopyOnWriteArrayList 像“复印店”:多人同时阅读一份报纸(读操作无锁)。有人要改内容时,先复印一份新报纸在副本上改,改完再替换原版(写时复制)。正在读旧版的人不受影响。
三、阻塞队列核心机制
3.1 什么是 BlockingQueue?
BlockingQueue是java.util.concurrent包下的线程安全队列接口,专为生产者-消费者模型设计。
核心特性:
- 线程安全:所有实现类保证多线程并发操作的安全性
- 阻塞插入:队列满时,
put(e)阻塞生产者线程 - 阻塞移除:队列空时,
take()阻塞消费者线程 - 超时支持:
offer(e, timeout, unit)和poll(timeout, unit)支持超时阻塞 - 容量限制:分为有界队列(固定容量)和无界队列(理论无限容量)
3.2 核心 API 速记
| 操作类型 | 队列满/空时行为 | 插入方法 | 移除方法 | 检查方法 |
|---|---|---|---|---|
| 抛出异常 | 立即抛异常 | add(e) | remove() | element() |
| 返回特殊值 | 返回 false/null | offer(e) | poll() | peek() |
| 阻塞 | 阻塞直到成功 | put(e) | take() | — |
| 超时 | 超时返回 false/null | offer(e, time, unit) | poll(time, unit) | — |
3.3 五大实现类对比
| 实现类 | 底层结构 | 容量 | 核心特点 | 适用场景 |
|---|---|---|---|---|
| ArrayBlockingQueue | 数组 | 有界(必须指定) | 公平锁可选,内存紧凑 | 固定容量生产者-消费者 |
| LinkedBlockingQueue | 链表 | 可选有界(默认无界) | 生产者和消费者用独立锁,吞吐量高 | 高吞吐任务队列 |
| SynchronousQueue | 无存储 | 容量为 0 | 手递手,生产等消费 | 线程池(CachedThreadPool) |
| PriorityBlockingQueue | 堆(数组) | 无界 | 按优先级排序 | 优先级任务调度 |
| DelayQueue | 堆 + 延迟 | 无界 | 元素需实现Delayed,到期才能取出 | 定时任务、订单超时关闭 |
🔑 关键区别:
- Array vs Linked:Array 用一把锁,Linked 用两把锁(
takeLock+putLock),吞吐量更高 - 默认容量:
new LinkedBlockingQueue()是无界(Integer.MAX_VALUE),极易 OOM! - SynchronousQueue:容量为 0,不存储元素,生产者直接等待消费者接收
3.4 源码片段:ArrayBlockingQueue 的 put/take(锁+条件)
// ArrayBlockingQueue 核心:一把锁 + 两个条件publicclassArrayBlockingQueue<E>{finalReentrantLocklock;// 唯一锁privatefinalConditionnotEmpty;// 队列非空条件privatefinalConditionnotFull;// 队列未满条件publicvoidput(Ee)throwsInterruptedException{lock.lockInterruptibly();try{while(count==items.length)// 队列满 → 等待notFull.await();enqueue(e);}finally{lock.unlock();}}publicEtake()throwsInterruptedException{lock.lockInterruptibly();try{while(count==0)// 队列空 → 等待notEmpty.await();returndequeue();}finally{lock.unlock();}}}3.5 源码片段:LinkedBlockingQueue 的 put/take(双锁设计)
// LinkedBlockingQueue 核心:两把锁,分离生产与消费publicclassLinkedBlockingQueue<E>{privatefinalReentrantLocktakeLock=newReentrantLock();// 消费者锁privatefinalReentrantLockputLock=newReentrantLock();// 生产者锁privatefinalConditionnotEmpty=takeLock.newCondition();privatefinalConditionnotFull=putLock.newCondition();publicvoidput(Ee)throwsInterruptedException{putLock.lockInterruptibly();// 只锁生产者try{while(count==capacity)notFull.await();enqueue(e);}finally{putLock.unlock();}}publicEtake()throwsInterruptedException{takeLock.lockInterruptibly();// 只锁消费者try{while(count==0)notEmpty.await();returndequeue();}finally{takeLock.unlock();}}}💡双锁设计:生产者和消费者可同时操作,互不阻塞,吞吐量更高。
四、CopyOnWriteArrayList:读多写少的并发 List
4.1 核心思想
CopyOnWriteArrayList是线程安全的ArrayList变体,核心思想是写时复制(Copy-On-Write):
写操作:加锁复制一份新数组,修改完成后替换原数组
读操作:完全无锁,直接读当前数组
4.2 源码片段
publicclassCopyOnWriteArrayList<E>{privatetransientvolatileObject[]array;// volatile 保证可见性// 读操作:无锁!publicEget(intindex){returnget(getArray(),index);// 直接读,不加锁}// 写操作:加锁 + 复制publicbooleanadd(Ee){finalReentrantLocklock=this.lock;lock.lock();// 1. 加锁try{Object[]elements=getArray();intlen=elements.length;Object[]newElements=Arrays.copyOf(elements,len+1);// 2. 复制newElements[len]=e;// 3. 修改副本setArray(newElements);// 4. 替换引用returntrue;}finally{lock.unlock();}}}4.3 优缺点与适用场景
| 维度 | 说明 |
|---|---|
| ✅ 优点 | 读操作无锁,读多写少场景性能极高;迭代器不抛ConcurrentModificationException |
| ❌ 缺点 | 每次写都复制整个数组,写操作代价高;数据弱一致性(读可能读到旧数据) |
| 适用场景 | 读多写少:配置列表、黑白名单、缓存数据等 |
| 不适用场景 | 写操作频繁的场景(如实时计数器) |
五、ConcurrentLinkedQueue:CAS 无锁非阻塞队列
5.1 核心思想
ConcurrentLinkedQueue是无界、非阻塞、线程安全的 FIFO 队列,基于链表 + CAS实现。
与 BlockingQueue 的本质区别:
BlockingQueue:用锁实现阻塞(put/take可阻塞线程)ConcurrentLinkedQueue:用CAS实现非阻塞(offer/poll立即返回,不阻塞)
5.2 核心特点
| 特性 | 说明 |
|---|---|
| 线程安全 | 多线程并发插入/删除不会导致数据不一致 |
| 非阻塞 | 使用 CAS 操作,避免锁竞争导致的线程阻塞 |
| 无界 | 可动态扩展,理论无限容量(受内存限制) |
| 高性能 | 无锁设计,高并发下性能优于阻塞队列 |
5.3 核心方法
ConcurrentLinkedQueue<String>queue=newConcurrentLinkedQueue<>();queue.offer("task1");// 插入,立即返回 truequeue.add("task2");// 插入,失败抛异常Stringtask=queue.poll();// 移除并返回头元素,空返回 nullStringtask=queue.peek();// 返回头元素不移除,空返回 null六、线程池中的阻塞队列选型
| 线程池 | 默认阻塞队列 | 特点 |
|---|---|---|
FixedThreadPool | LinkedBlockingQueue | 无界队列,任务量需可控,否则 OOM |
CachedThreadPool | SynchronousQueue | 不存任务,直接交给线程,无任务时销毁线程 |
SingleThreadExecutor | LinkedBlockingQueue | 同 Fixed,单线程 |
ScheduledThreadPool | DelayedWorkQueue | 延迟队列,支持定时/周期任务 |
七、生产级避坑清单
✅ 阻塞队列与并发容器使用规范 1. LinkedBlockingQueue 无参构造默认无界(Integer.MAX_VALUE)→ 务必指定容量,否则 OOM 2. ArrayBlockingQueue 必须指定容量,创建时明确队列大小 3. SynchronousQueue 不存储元素,offer() 无消费者立即返回 false,必须用 put() 4. CopyOnWriteArrayList 写操作频繁时性能极差 → 仅用于读多写少场景 5. ConcurrentLinkedQueue 的 size() 是 O(n) 遍历 → 不要频繁调用 6. 阻塞队列的 take() 会阻塞线程 → 确保有对应的生产者,否则线程永久阻塞八、面试高频考点
Q1:BlockingQueue 的核心方法有哪些?区别是什么?
四类方法:① 抛异常(
add/remove/element);② 返回特殊值(offer/poll/peek);③ 阻塞(put/take);④ 超时(offer(time)/poll(time))。put/take是阻塞队列的核心方法。
Q2:ArrayBlockingQueue 和 LinkedBlockingQueue 的区别?
Array 基于数组,必须指定容量,用一把锁,内存紧凑。Linked 基于链表,可选容量(默认无界),生产者和消费者用独立锁,吞吐量更高。高并发场景 Linked 更优,但需注意无界队列可能 OOM。
Q3:SynchronousQueue 的作用?
容量为 0 的阻塞队列,不存储元素,生产者插入必须等待消费者接收。常用于
Executors.newCachedThreadPool(),实现任务直接传递给线程。
Q4:CopyOnWriteArrayList 的原理和适用场景?
写时复制:写操作加锁复制整个数组,修改后替换原数组;读操作完全无锁。适用于读多写少场景(如配置列表、黑白名单)。写频繁时性能极差。
Q5:ConcurrentLinkedQueue 和 BlockingQueue 的区别?
ConcurrentLinkedQueue是非阻塞队列,基于 CAS 无锁实现;BlockingQueue是阻塞队列,基于锁 + Condition 实现。前者适合高并发任务队列,后者适合生产者-消费者模型。
面试官追问陷阱(加分题)
追问1:“new LinkedBlockingQueue()不指定容量会怎样?”
👉 默认容量是
Integer.MAX_VALUE,近似无界。如果生产者速度持续快于消费者,队列会无限膨胀,最终OOM。生产环境务必指定容量。
追问2:“CopyOnWriteArrayList 的迭代器会抛ConcurrentModificationException吗?”
👉不会。迭代器基于创建时的数组快照,遍历期间不感知后续修改,因此不会抛异常。代价是读不到最新数据(弱一致性)。
追问3:“ConcurrentLinkedQueue的size()为什么不准确?”
👉
size()需要遍历整个链表累加计数,时间复杂度 O(n),且遍历过程中可能有并发修改,结果不精确。高并发场景下慎用。
九、练习题
源码推导:
ArrayBlockingQueue和LinkedBlockingQueue的锁机制有什么区别?各有什么优缺点?场景设计:某系统需要维护一份读频率极高、几乎不修改的敏感词列表,选用什么并发容器最合适?
代码分析:下面的代码有什么问题?
LinkedBlockingQueue<String>queue=newLinkedBlockingQueue<>();for(inti=0;i<1000000;i++){queue.put("task"+i);}
📊 你的学习进度
- 当前:第50篇 / 共108篇 ·进阶篇:集合框架源码解析(第45~50篇)
- ✅ 已完成:基础篇44篇 + 第45~50篇(集合框架源码系列收官!)
- 📖 正在学:第50篇
- ⏳ 待学习:第51~108篇
👉 📚 完整目录 & 学习指南 | 🔥 订阅本专栏,不错过每一篇
👉 下一篇文章预告
🚀下一篇:《第51篇:线程生命周期与创建方式》
内容简介:线程的 6 种状态(NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED)、状态流转图、创建线程的 4 种方式(Thread、Runnable、Callable、线程池)。
👉第45~50篇集合框架源码系列正式收官!下一阶段开启多线程与高并发专题!
📌《Java 100 天进阶之路 | 从入门到上岗就业》每天一篇,建议收藏 + 关注,一起100天拿offer!
👉 点击关注我,更新后第一时间收到推送!
