阻塞队列对比
候选人小周在面试阿里 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 的主要阻塞队列:
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:
P7 级别:队列特性对比
ArrayBlockingQueue vs LinkedBlockingQueue:
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:保护出队操作
两把锁可以并发执行,减少了入队和出队之间的竞争。notEmpty 和 notFull 条件分别在对应的锁下等待。
追问 2:LinkedTransferQueue 相比其他队列的优势是什么?
- 支持
transfer() 方法:生产者将元素直接传给消费者(不存储),更直接
tryTransfer():非阻塞版本的 transfer
- 吞吐量比 LinkedBlockingQueue 更高(无锁算法 CAS)
二、选型决策 🟡
2.1 选型表
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 改变顺序。