常见的阻塞队列有哪几种?怎么选?

#Java#面试八股

常见的阻塞队列有哪几种?怎么选?

结论先行:java.util.concurrent 提供 7 种阻塞队列,选型只需回答三个问题:要不要有界、要不要排序、要不要按时间延迟。线程池里最常用的是 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 三种。

1. 七种队列对照

队列底层结构是否有界排序典型场景
ArrayBlockingQueue数组 + 单锁双条件有界(必填容量)FIFO线程池、稳定可控的缓冲
LinkedBlockingQueue链表 + 双锁可选(默认 Integer.MAX_VALUE)FIFO吞吐优先,必须显式给容量
SynchronousQueue无存储,配对传递容量 0无CachedThreadPool、直接交接
PriorityBlockingQueue二叉堆数组无界按 Comparator优先级任务调度
DelayQueue优先队列 + 延迟无界到期时间定时任务、订单超时关闭
LinkedTransferQueue链表无界FIFO高吞吐传递,transfer() 等消费者
LinkedBlockingDeque双向链表可选FIFO/LIFO工作窃取式双端取任务

2. 四种典型语义

ArrayBlockingQueue(3)        SynchronousQueue
┌───┬───┬───┐                put ──┐
│ a │ b │ c │  满了 put 阻塞        ├──▶ 必须有人正等着 take
└───┴───┴───┘   空了 take 阻塞   take ─┘   否则 put 阻塞(无缓冲)

PriorityBlockingQueue        DelayQueue
take 永远返回最小元素         take 只返回"到期"的元素
(无界,生产过快会 OOM)      未到期元素即使队首也要继续等

3. 常用写法

// 1. 有界数组队列:线程池首选,容量固定、内存可控
BlockingQueue<Task> q1 = new ArrayBlockingQueue<>(1000);

// 2. 链表队列:吞吐更高,但务必显式指定容量
BlockingQueue<Task> q2 = new LinkedBlockingQueue<>(1000);

// 3. 直接交接:不存储,生产者必须等到消费者
BlockingQueue<Task> q3 = new SynchronousQueue<>();

// 4. 延迟队列:元素实现 Delayed,到期才能被取出
public class OrderTimeout implements Delayed {
    private final long expireAt;                 // 绝对时间戳(毫秒)
    public long getDelay(TimeUnit unit) {
        return unit.convert(expireAt - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }
    public int compareTo(Delayed o) {
        return Long.compare(this.expireAt,
                ((OrderTimeout) o).expireAt);
    }
}
DelayQueue<OrderTimeout> delayQ = new DelayQueue<>();
delayQ.put(new OrderTimeout(System.currentTimeMillis() + 30 * 60_000L));
OrderTimeout expired = delayQ.take();          // 阻塞到最早的元素到期

线程池里换队列,行为会完全不同:

队列效果
ArrayBlockingQueue(n)满了就触发拒绝策略,背压明确
LinkedBlockingQueue() 无参maximumPoolSize 永远用不上,任务无限堆积 → OOM
SynchronousQueue没有缓冲,来一个任务就必须有线程接,接不住就扩容或拒绝

4. 选型决策

需要按优先级/到期时间取任务吗?
├── 需要 → DelayQueue(按时间)/ PriorityBlockingQueue(按优先级)
└── 不需要 → 需要缓冲吗?
    ├── 不需要缓冲,希望直接交给线程 → SynchronousQueue
    └── 需要缓冲 → 容量能预估吗?
        ├── 能 → ArrayBlockingQueue(内存连续、边界清晰)
        └── 不能 → LinkedBlockingQueue(但必须设上限)

5. 常见追问

追问一:ArrayBlockingQueue 和 LinkedBlockingQueue 的性能差异? ArrayBlockingQueue 一把锁管进出,LinkedBlockingQueue 用 putLock/takeLock 两把锁分离生产和消费,并发更高;但链表要为每个节点分配对象,GC 压力大。量级不大时优先数组,因为内存可控。

追问二:SynchronousQueue 为什么给 CachedThreadPool 用? CachedThreadPool 的语义是"来任务就找空闲线程,没有就新建"。SynchronousQueue 容量为 0,offer 失败即说明没有空闲线程,于是扩容——这正好实现了"按需扩容"。

追问三:PriorityBlockingQueue 有什么坑? 无界。生产速度长期高于消费速度时,堆会一直涨到 OOM;另外它不保证同优先级元素的顺序,且元素必须可比较(Comparable 或传 Comparator)。

追问四:DelayQueue 的 take() 是怎么等到期的? 队首元素的 getDelay() 大于 0 时,take() 会 awaitNanos(delay) 精确等待,而不是空转轮询。ScheduledThreadPoolExecutor 内部的 DelayedWorkQueue 是同一套思路的定制版。

小结

阻塞队列的选择本质是三个开关:有没有界、排不排序、延不延迟。工程上最稳的组合是"ArrayBlockingQueue + 明确容量 + 明确的拒绝策略"——边界清楚,出问题也可观测。

笔记加载中…