阻塞队列对比

候选人小周在面试阿里 P6 时,面试官问道:

"Java 有哪些阻塞队列?它们有什么区别?"

小周说:"有 ArrayBlockingQueue 和 LinkedBlockingQueue..."面试官追问:"SynchronousQueue 和其他队列有什么区别?"

小周答不上来。面试官继续:"什么场景用什么队列?"

小周彻底卡住了...

一、核心问题:阻塞队列对比 🔴

1.1 问题拆解

第一层:队列种类(有哪几种?)
  "Java 中的阻塞队列有哪些?它们各自的特点是什么?"
  考察点:Array/Liked/Priority/Synchronous/Delay/Transfer

第二层:底层实现(怎么实现?)
  "不同队列的底层数据结构是什么?"
  考察点:数组 vs 链表 vs 堆 vs 栈

第三层:阻塞机制(怎么做到?)
  "阻塞的 put/take 和非阻塞的 offer/poll 有什么区别?"
  考察点:ReentrantLock + Condition、LockSupport.park

第四层:选型决策(怎么选?)
  "什么场景用什么队列?"
  考察点:容量、优先级、公平性

1.2 ❌ 错误示范

候选人原话 A:"ArrayBlockingQueue 比 LinkedBlockingQueue 快,因为用数组。"

问题诊断:两者各有优劣。ArrayBlockingQueue 用数组实现,内存连续,GC 友好,但容量固定。LinkedBlockingQueue 用链表实现,容量可选(默认 Integer.MAX_VALUE),但有额外的 Node 对象开销。

候选人原话 B:"SynchronousQueue 是一个队列。"

问题诊断:SynchronousQueue 实际上没有队列——它没有存储空间,每个 put 必须等待一个 take,反之亦然。

1.3 标准回答

P5 级别:五种阻塞队列

Java 的主要阻塞队列

队列底层结构是否有界特性
ArrayBlockingQueue数组有界FIFO,固定容量
LinkedBlockingQueue链表可选有界(默认 Integer.MAX_VALUE)FIFO,高吞吐量
PriorityBlockingQueue无界按优先级排序
DelayQueue优先级队列无界元素需实现 Delayed 接口
SynchronousQueue无存储直接传递,零容量
LinkedTransferQueue链表无界支持 transfer 操作
LinkedBlockingDeque双向链表可选有界双端队列

P6 级别:阻塞机制

阻塞的实现原理

// ArrayBlockingQueue 的 put() 实现
public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    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();
    }
}

offer/poll vs put/take

方法队列满/空时的行为
put(e) / take()阻塞等待
offer(e, timeout, unit) / poll(timeout, unit)超时等待
offer(e) / poll()非阻塞,立即返回 false/null
add(e)非阻塞,满则抛 IllegalStateException

P7 级别:队列特性对比

ArrayBlockingQueue vs LinkedBlockingQueue

维度ArrayBlockingQueueLinkedBlockingQueue
底层结构定长数组单向链表 + Node 对象
容量固定(构造时指定)可选(默认 Integer.MAX_VALUE)
一把锁(入队出队互斥)两把锁(入队锁 + 出队锁,可并发)
吞吐量较低较高(锁分段)
内存预分配,无需额外对象每个元素一个 Node 对象
GC友好(无额外对象)Node 对象增加 GC 压力

SynchronousQueue 的独特性

SynchronousQueue 没有存储空间。每个 put 必须等待一个 take,反之亦然:

SynchronousQueue<Integer> q = new SynchronousQueue<>();

// 线程 A
q.put(42);  // 阻塞,直到线程 B 调用 take()

// 线程 B
Integer v = q.take();  // 阻塞,直到线程 A 调用 put()

// exchange() 是更直接的版本
q.transfer(42);  // 阻塞,直到消费者接收并确认

DelayQueue 的使用场景

DelayQueue<DelayedTask> queue = new DelayQueue<>();

queue.put(new DelayedTask("task1", 1000));  // 1秒后到期
queue.put(new DelayedTask("task2", 3000));  // 3秒后到期

DelayedTask task = queue.take();  // 阻塞,直到有任务到期
// 返回最早到期的任务

PriorityBlockingQueue 的堆结构

PriorityBlockingQueue<Integer> pq = new PriorityBlockingQueue<>();
pq.put(3);
pq.put(1);
pq.put(2);
System.out.println(pq.take());  // 输出 1(最小堆)
System.out.println(pq.take());  // 输出 2
System.out.println(pq.take());  // 输出 3

【面试官心理】 这道题我能问到 P7 级别,是因为不同阻塞队列有不同的内部实现和数据结构。能比较 Array vs Linked 锁策略的候选人说明他理解了并发优化。能说清 SynchronousQueue 和 DelayQueue 使用场景的候选人说明他有工程经验。

1.4 追问升级

追问 1:LinkedBlockingQueue 的两把锁是怎么工作的?

  • putLock:保护入队操作
  • takeLock:保护出队操作

两把锁可以并发执行,减少了入队和出队之间的竞争。notEmptynotFull 条件分别在对应的锁下等待。

追问 2:LinkedTransferQueue 相比其他队列的优势是什么?

  1. 支持 transfer() 方法:生产者将元素直接传给消费者(不存储),更直接
  2. tryTransfer():非阻塞版本的 transfer
  3. 吞吐量比 LinkedBlockingQueue 更高(无锁算法 CAS)

二、选型决策 🟡

2.1 选型表

场景推荐队列理由
生产者-消费者,固定容量ArrayBlockingQueue容量可控,性能好
生产者-消费者,需要高吞吐LinkedBlockingQueue双锁并发
优先级任务调度PriorityBlockingQueue堆结构,最小/最大堆
延迟任务DelayQueue到期才可取出
直接传递,无缓冲SynchronousQueueExchanger 替代
最高吞吐量LinkedTransferQueue无锁算法

2.2 常见错误

// 错误:LinkedBlockingQueue 默认无界,高并发下 OOM
LinkedBlockingQueue<Task> q = new LinkedBlockingQueue<>();
// 任务堆积 → OOM

// 正确:使用有界队列
LinkedBlockingQueue<Task> q = new LinkedBlockingQueue<>(1000);

三、生产避坑 🟡

3.1 队列容量设置不当

队列容量太小 → 拒绝策略频繁触发 队列容量太大 → 任务堆积,延迟增加,内存 OOM

经验公式队列容量 ≈ 线程数 × 期望缓冲时间(秒)

3.2 DelayQueue 的元素必须实现 Delayed

// 错误:直接使用 Integer
DelayQueue<Integer> q = new DelayQueue<>();
q.put(1);  // 编译错误!Integer 没有实现 Delayed

// 正确:包装为 Task
class Task implements Delayed {
    String name;
    long startTime;
>
>    public long getDelay(TimeUnit unit) {
>        return unit.convert(startTime - System.nanoTime(), TimeUnit.NANOSECONDS);
>    }
>
>    public int compareTo(Delayed o) {
>        return Long.compare(this.startTime, ((Task)o).startTime);
>    }
}
💡

面试加分点:能说出"LinkedTransferQueue 的 transfer() 底层使用了一个叫做"双重栈"的数据结构,在公平模式下使用 FIFO 队列,性能和直接传递的效率都很高",说明他对无锁队列有深入了解。

⚠️

面试陷阱:被问到"PriorityBlockingQueue 是最大堆还是最小堆",很多人会说"最大堆"。准确答案是:最小堆(自然顺序)。可以通过构造函数传入 Comparator 改变顺序。