← 返回题目列表

阻塞队列 BlockingQueue 的原理是什么?有哪些常见实现?

高频 中等 第 7 / 31 题 更新于 2026/07/26
BlockingQueue阻塞队列生产者消费者线程池

简化版

BlockingQueue 是支持「阻塞式存取」的线程安全队列:队列满时 put 会阻塞生产者,队列空时 take 会阻塞消费者,直到有空间/有元素才唤醒。它把「生产者-消费者」模型中最麻烦的「等待、唤醒、加锁」全封装好了,用起来只有 put/take 两个方法。底层多用 ReentrantLock + Condition(notFull、notEmpty 两个条件)实现。它也是线程池任务队列的核心,Java 提供了 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue、DelayQueue 等实现。

详细版

四组方法,区别在「队列满/空时的反应」:

操作抛异常返回特殊值阻塞超时阻塞
入队add(满抛异常)offer(满返回 false)put(满则阻塞)offer(e, time)
出队remove(空抛异常)poll(空返回 null)take(空则阻塞)poll(time)

生产者消费者最常用 put / take——它们自动阻塞,不用手写 wait/notify。

常见实现

实现底层结构有界特点
ArrayBlockingQueue数组有界(必须指定容量)一把锁,可选公平
LinkedBlockingQueue链表可选(默认 Integer.MAX_VALUE 近似无界)读写两把锁,吞吐高
SynchronousQueue无容量容量 0每个 put 必须等一个 take 配对,直接手递手
PriorityBlockingQueue无界按优先级出队,非 FIFO
DelayQueue无界元素到期才能取,用于延迟任务/定时
BlockingQueue<Task> queue = new ArrayBlockingQueue<>(100);

// 生产者:队列满则阻塞等待
queue.put(task);

// 消费者:队列空则阻塞等待
Task task = queue.take();

⚠️ LinkedBlockingQueue 不指定容量时默认是 Integer.MAX_VALUE,近似无界。用它当线程池队列时,任务会无限堆积,maximumPoolSize 形同虚设,还可能 OOM——生产环境务必指定容量或用有界队列。

完整版教学

一、为什么需要阻塞队列:生产者消费者的痛点

生产者-消费者是最经典的并发模型:一批线程生产数据放进缓冲区,另一批消费。手写这个模型要处理三件麻烦事:

① 缓冲区满了,生产者要停下来等(不能丢数据,也不能空转 CPU)
② 缓冲区空了,消费者要停下来等(不能取到 null)
③ 存取要加锁(防止并发修改),且等待/唤醒要精确(满了唤醒生产者、空了唤醒消费者)

自己用 synchronized + wait/notify 写,很容易漏掉「唤醒错对象」「虚假唤醒」「锁范围不对」等 bug。BlockingQueue 把这三件事全封装了put 遇满自动挂起、take 遇空自动挂起,有空间/有数据自动唤醒对应线程。使用者只管 put/take,并发协调交给队列。这就是它存在的意义——把生产者消费者从「手写陷阱」变成「两个方法」。

二、底层原理:一把锁 + 两个 Condition

ArrayBlockingQueue 为例,核心是 ReentrantLock 加两个条件队列 notEmptynotFull

final ReentrantLock lock;
Condition notEmpty;   // 消费者在这等"队列非空"
Condition notFull;    // 生产者在这等"队列非满"

put(e):  lock → while(满) notFull.await()  → 入队 → notEmpty.signal() → unlock
take():  lock → while(空) notEmpty.await() → 出队 → notFull.signal()  → unlock

精髓在于用两个 Condition 分开管理两类等待者:生产者只在 notFull 上等,消费者只在 notEmpty 上等。入队后精确唤醒 notEmpty(叫醒消费者),出队后精确唤醒 notFull(叫醒生产者)——不会像 notifyAll 那样把不相关的线程全叫醒空转。用 while 而非 if 判断条件,是为了防「虚假唤醒」:醒来后重新检查条件,不满足继续等。

三、ArrayBlockingQueue vs LinkedBlockingQueue

这是最常考的一对对比:

维度ArrayBlockingQueueLinkedBlockingQueue
结构定长数组(循环)链表
容量必须指定(有界)可选,默认 Integer.MAX_VALUE
一把锁(put/take 互斥)两把锁(putLock、takeLock 分离)
吞吐较低(存取抢同一把锁)较高(存取可并行)
内存创建时一次分配按需创建节点,有额外对象开销

关键差异是锁的数量:ArrayBlockingQueue 存和取用同一把锁,同一时刻只能一个操作;LinkedBlockingQueue 用「双锁」——生产者用 putLock、消费者用 takeLock,存取可以真正并行,所以高并发下吞吐更高。代价是链表每个节点要额外 new 对象。选型:要严格有界、内存可控用 ArrayBlockingQueue;要高吞吐且能指定容量用 LinkedBlockingQueue(记得设容量!)。

四、SynchronousQueue:没有容量的「手递手」

SynchronousQueue 很特别——容量为 0,不存储任何元素。每个 put 必须等一个 take 来配对,反之亦然,像「一手交钱一手交货」:

put 线程:我要交一个任务 → 阻塞等待,直到有 take 线程来接
take 线程:我要接一个任务 → 阻塞等待,直到有 put 线程来给
两者相遇 → 直接手递手传递,不经过任何缓冲

它是 Executors.newCachedThreadPool() 的任务队列:来一个任务就要求立刻有线程接手,没有空闲线程就新建。因为「不缓冲、要求即时配对」,特别适合任务需要被立即处理的场景。反过来说,它不能堆积任务,所以搭配的必须是「线程数可无限增长」的池。

五、PriorityBlockingQueue 与 DelayQueue:带序的阻塞队列

两个「非 FIFO」的特殊实现:

  • PriorityBlockingQueue:底层是二叉堆,出队按优先级(最小/自定义 Comparator 先出)而非先进先出。无界(会自动扩容,所以 put 不阻塞,只有 take 空时阻塞)。用于「任务有优先级」的调度。
  • DelayQueue:元素必须实现 Delayed 接口,只有到期的元素才能被 take,没到期就阻塞。底层是 PriorityBlockingQueue(按到期时间排序,最早到期的在堆顶)。
DelayQueue 应用:
- 延迟任务:订单 30 分钟未支付自动取消 → 下单时放入延迟 30 分钟的元素
- 缓存过期:到期 key 自动淘汰
- 定时重试:失败任务延迟 N 秒后重新可取
取出逻辑:take 时看堆顶元素到期没,没到期就 await 剩余时间

面试常问「怎么实现延迟队列/定时取消订单」——DelayQueue 是标准答案之一。

六、和线程池的关系:任务队列的选择

BlockingQueue 是 ThreadPoolExecutor 的第 5 个构造参数(workQueue),选哪种直接决定线程池行为:

队列线程池行为
有界 ArrayBlockingQueue队列满且线程到 max 后触发拒绝策略(推荐,可控)
无界 LinkedBlockingQueue任务无限堆积,maxPoolSize 失效,可能 OOM(FixedThreadPool 的坑)
SynchronousQueue不缓冲,来任务就要有线程接,配合无限扩容池(CachedThreadPool)

这也是「为什么不推荐用 Executors 快捷方法」的根源:newFixedThreadPool 用无界 LinkedBlockingQueue、newCachedThreadPool 用 SynchronousQueue 配 Integer.MAX_VALUE 最大线程数,两者都可能把内存或线程数撑爆。手动 new ThreadPoolExecutor 并传有界队列,才能让拒绝策略真正生效。

记忆钩子:「满则阻塞生产、空则阻塞消费,一把锁两 Condition;Array 有界一把锁、Linked 双锁高吞吐、Synchronous 手递手、Delay 到期才取」

七、常见误区与追问

  • 误区:LinkedBlockingQueue 是无界的所以更好用。 默认近似无界(Integer.MAX_VALUE)会导致任务无限堆积甚至 OOM,当线程池队列时必须显式指定容量。
  • 误区:BlockingQueue 的所有方法都会阻塞。 只有 put/take(和带超时的 offer/poll)阻塞;add/remove 满空时抛异常,offer/poll 返回特殊值,四组行为不同。
  • 误区:SynchronousQueue 能存一个元素。 它容量为 0,不存任何元素,put 必须等 take 配对,是「传递」而非「缓冲」。
  • 误区:PriorityBlockingQueue 的 put 会因队列满而阻塞。 它是无界的(自动扩容),put 不阻塞;只有 take 在队列空时阻塞。
  • 追问:BlockingQueue 怎么防止虚假唤醒? 等待条件用 while 循环判断而非 if,线程被唤醒后重新检查条件,不满足继续 await。
  • 追问:ArrayBlockingQueue 为什么用一把锁而 LinkedBlockingQueue 用两把? 数组队列头尾操作共享同一状态难以拆分;链表队列头尾是独立节点,可用 putLock/takeLock 让存取并行,提升吞吐。
  • 追问:如何用 BlockingQueue 实现延迟任务? 用 DelayQueue,元素实现 Delayed 接口按到期时间排序,take 只返回已到期元素,适合定时取消、缓存过期等。

八、加强记忆

BlockingQueue 的价值是把「生产者-消费者」从手写 wait/notify 的陷阱变成 put/take 两个方法:队列满则阻塞生产者、队列空则阻塞消费者,底层典型实现是 ReentrantLock + notFull/notEmpty 两个 Condition——生产者只在 notFull 等、消费者只在 notEmpty 等,存取后精确唤醒对方、用 while 防虚假唤醒。四组方法要分清:add/remove 抛异常、offer/poll 返回特殊值、put/take 阻塞、带超时的 offer/poll。实现按场景选:ArrayBlockingQueue(数组、有界、一把锁)、LinkedBlockingQueue(链表、双锁高吞吐、务必设容量)、SynchronousQueue(容量 0、手递手、CachedThreadPool 用它)、PriorityBlockingQueue(按优先级)、DelayQueue(到期才取,做延迟任务)。它还是线程池的任务队列,「用有界队列让拒绝策略生效」是关键工程原则。一句话「满阻塞产、空阻塞费、Array 一把锁、Linked 双锁、Sync 手递手、Delay 到期取」。