《Java 100 天进阶之路》第50篇:阻塞队列与并发容器(2026版)

第50篇:阻塞队列与并发容器(2026版)

📌 系列导航 :《Java 100 天进阶之路》完整目录 |

⬅️ 上一篇:第49篇:ConcurrentHashMap原理 |

➡️ 下一篇:第51篇:线程生命周期与创建方式


文章目录


🗺️ 本文阅读地图(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(锁+条件)
java 复制代码
// ArrayBlockingQueue 核心:一把锁 + 两个条件
public class ArrayBlockingQueue<E> {
    final ReentrantLock lock;          // 唯一锁
    private final Condition notEmpty;  // 队列非空条件
    private final Condition notFull;   // 队列未满条件

    public void put(E e) throws InterruptedException {
        lock.lockInterruptibly();
        try {
            while (count == items.length)  // 队列满 → 等待
                notFull.await();
            enqueue(e);
        } finally { lock.unlock(); }
    }

    public E take() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            while (count == 0)             // 队列空 → 等待
                notEmpty.await();
            return dequeue();
        } finally { lock.unlock(); }
    }
}
3.5 源码片段:LinkedBlockingQueue 的 put/take(双锁设计)
java 复制代码
// LinkedBlockingQueue 核心:两把锁,分离生产与消费
public class LinkedBlockingQueue<E> {
    private final ReentrantLock takeLock = new ReentrantLock();  // 消费者锁
    private final ReentrantLock putLock = new ReentrantLock();    // 生产者锁
    private final Condition notEmpty = takeLock.newCondition();
    private final Condition notFull = putLock.newCondition();

    public void put(E e) throws InterruptedException {
        putLock.lockInterruptibly();      // 只锁生产者
        try {
            while (count == capacity) notFull.await();
            enqueue(e);
        } finally { putLock.unlock(); }
    }

    public E take() throws InterruptedException {
        takeLock.lockInterruptibly();     // 只锁消费者
        try {
            while (count == 0) notEmpty.await();
            return dequeue();
        } finally { takeLock.unlock(); }
    }
}

💡 双锁设计 :生产者和消费者可同时操作,互不阻塞,吞吐量更高。


四、CopyOnWriteArrayList:读多写少的并发 List

4.1 核心思想

CopyOnWriteArrayList 是线程安全的 ArrayList 变体,核心思想是写时复制(Copy-On-Write):

写操作 :加锁复制一份新数组,修改完成后替换原数组

读操作:完全无锁,直接读当前数组

4.2 源码片段
java 复制代码
public class CopyOnWriteArrayList<E> {
    private transient volatile Object[] array;  // volatile 保证可见性

    // 读操作:无锁!
    public E get(int index) {
        return get(getArray(), index);  // 直接读,不加锁
    }

    // 写操作:加锁 + 复制
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();                    // 1. 加锁
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1); // 2. 复制
            newElements[len] = e;       // 3. 修改副本
            setArray(newElements);      // 4. 替换引用
            return true;
        } 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 核心方法
java 复制代码
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();

queue.offer("task1");   // 插入,立即返回 true
queue.add("task2");     // 插入,失败抛异常
String task = queue.poll();  // 移除并返回头元素,空返回 null
String task = queue.peek();  // 返回头元素不移除,空返回 null

六、线程池中的阻塞队列选型

线程池 默认阻塞队列 特点
FixedThreadPool LinkedBlockingQueue 无界队列,任务量需可控,否则 OOM
CachedThreadPool SynchronousQueue 不存任务,直接交给线程,无任务时销毁线程
SingleThreadExecutor LinkedBlockingQueue 同 Fixed,单线程
ScheduledThreadPool DelayedWorkQueue 延迟队列,支持定时/周期任务

七、生产级避坑清单

text 复制代码
✅ 阻塞队列与并发容器使用规范
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),且遍历过程中可能有并发修改,结果不精确。高并发场景下慎用。


九、练习题

  1. 源码推导 :ArrayBlockingQueue 和 LinkedBlockingQueue 的锁机制有什么区别?各有什么优缺点?

  2. 场景设计 :某系统需要维护一份读频率极高、几乎不修改的敏感词列表,选用什么并发容器最合适?

  3. 代码分析:下面的代码有什么问题?

    java 复制代码
    LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();
    for (int i = 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!

👉 点击关注我,更新后第一时间收到推送!

相关推荐
silvamux_10 分钟前
Spring Boot 调用大模型 api 最小可跑教程
java·spring boot·后端
今年下半年13 分钟前
【Spring Boot 】通过 OpenFeign 请求第三方接口
java·spring boot·后端·openfeign
范什么特西22 分钟前
亲和路由(inspect)
java
她说可以呀24 分钟前
Docker 镜像的两种创建方式
java·分布式·docker
霸道流氓气质34 分钟前
AgentFlow 入门与实战:Task-Based 并发、EWMA 动态调度与四道门禁的 Java 生产级编排
java·开发语言
愿得一猪蹄1 小时前
# Java线程池实战,别再new Thread了
java
ee又momo1 小时前
# 深入理解Java注解,从使用到自定义
java
霸道流氓气质1 小时前
AI智能博客平台实战:RAG增强写作、多Agent协作与语义检索的Vue3 + Spring AI全栈实现
java·人工智能·spring
打工仔折腾 AI1 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
哦虎!1 小时前
【JavaEE】网络原理 - TCP/IP
java