Queue 与 Deque
问题
Java 中有哪些队列实现?PriorityQueue、ArrayDeque、BlockingQueue 各有什么特点?
答案
Queue 接口
Queue 是先进先出(FIFO)的集合,提供两套 API:
| 操作 | 抛异常 | 返回特殊值 |
|---|---|---|
| 插入 | add(e) → IllegalStateException | offer(e) → false |
| 移除 | remove() → NoSuchElementException | poll() → null |
| 查看 | element() → NoSuchElementException | peek() → null |
PriorityQueue(优先队列)
基于最小堆(数组实现),元素按自然顺序或 Comparator 排序,队首始终是最小元素:
PriorityQueueDemo.java
// 最小堆(默认)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(3);
minHeap.offer(1);
minHeap.offer(2);
minHeap.poll(); // 1(最小的先出)
minHeap.poll(); // 2
// 最大堆
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
maxHeap.offer(3);
maxHeap.offer(1);
maxHeap.offer(2);
maxHeap.poll(); // 3(最大的先出)
// 自定义排序
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);
| 操作 | 时间复杂度 |
|---|---|
offer(e) / add(e) | |
poll() / remove() | |
peek() | |
remove(Object) |
注意事项
- PriorityQueue 不是线程安全的,多线程用
PriorityBlockingQueue - 不保证迭代顺序:
iterator()遍历不是按优先级顺序 - 不允许
null元素
ArrayDeque(双端队列)
基于循环数组实现,既可以当栈用也可以当队列用,性能优于 LinkedList 和 Stack:
ArrayDequeDemo.java
// 当队列使用(FIFO)
Deque<String> queue = new ArrayDeque<>();
queue.offer("a"); // 入队(尾部)
queue.offer("b");
queue.poll(); // "a"(头部出队)
// 当栈使用(LIFO)— 替代 Stack 类
Deque<String> stack = new ArrayDeque<>();
stack.push("a"); // 入栈(头部)
stack.push("b");
stack.pop(); // "b"(头部出栈)
为什么推荐 ArrayDeque 替代 Stack 和 LinkedList?
| 维度 | ArrayDeque | LinkedList | Stack |
|---|---|---|---|
| 底层结构 | 循环数组 | 双向链表 | 数组 |
| 缓存友好 | 好 | 差 | 好 |
| 内存开销 | 低 | 高(每节点两个指针) | 低 |
| null 支持 | 不允许 | 允许 | 允许 |
| 线程安全 | 否 | 否 | 是(synchronized) |
| 推荐度 | 首选 | 仅需 List+Deque 时 | 已废弃 |
BlockingQueue(阻塞队列)
线程安全的队列,当队列为空时取操作阻塞,当队列满时放操作阻塞。是生产者-消费者模式的核心:
| 实现类 | 底层结构 | 有界 | 特点 |
|---|---|---|---|
ArrayBlockingQueue | 数组 | 必须指定容量 | 公平/非公平锁 |
LinkedBlockingQueue | 链表 | 可选(默认 MAX) | 两把锁(读写分离) |
PriorityBlockingQueue | 堆 | 无界 | 优先级排序 |
SynchronousQueue | 无 | 0 容量 | 直接传递(Executors.newCachedThreadPool 使用) |
DelayQueue | 堆 | 无界 | 延迟获取(定时任务) |
BlockingQueueDemo.java
// 生产者-消费者模式
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
// 生产者线程
new Thread(() -> {
try {
queue.put("message"); // 队列满时阻塞
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
// 消费者线程
new Thread(() -> {
try {
String msg = queue.take(); // 队列空时阻塞
System.out.println("收到: " + msg);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
常见面试问题
Q1: Stack 为什么不推荐使用?用什么替代?
答案:
Stack 继承自 Vector,所有方法都有 synchronized,单线程使用有不必要的性能开销。且 Stack 继承 Vector 是设计错误(栈不应该有 get(i) 等随机访问方法)。替代方案:Deque<E> stack = new ArrayDeque<>()。
Q2: ArrayBlockingQueue 和 LinkedBlockingQueue 的区别?
答案:
| 维度 | ArrayBlockingQueue | LinkedBlockingQueue |
|---|---|---|
| 底层 | 固定数组 | 链表 |
| 容量 | 必须指定 | 可选(默认 Integer.MAX_VALUE) |
| 锁 | 一把锁 | 两把锁(putLock + takeLock) |
| GC | 无额外对象 | 每次 put 创建 Node 对象 |
| 性能 | 中等 | 高并发下更好(读写分离) |
Q3: PriorityQueue 的底层实现?
答案:
基于数组实现的最小堆(完全二叉树)。对于数组下标 i 的节点:
- 父节点:
(i - 1) / 2 - 左孩子:
2 * i + 1 - 右孩子:
2 * i + 2
插入时将元素放到数组末尾,然后上浮(sift up);删除堆顶时将末尾元素放到堆顶,然后下沉(sift down)。
Q4: SynchronousQueue 有什么用?
答案:
SynchronousQueue 不存储元素(容量为 0),每个 put 必须等待对应的 take,反之亦然。相当于一个直接传递的管道。Executors.newCachedThreadPool() 内部使用 SynchronousQueue,新任务到来时直接传递给空闲线程或创建新线程。