一、Queue 接口体系概览
Java 提供了一个 Queue 接口:
java
public interface Queue<E> extends Collection<E>
常用实现如下:
Collection
└── Queue
├── Deque(双端队列)
│ ├── ArrayDeque
│ └── LinkedList
├── PriorityQueue(优先队列)
└── BlockingQueue(并发阻塞队列)
├── LinkedBlockingQueue
├── ArrayBlockingQueue
├── SynchronousQueue
└── DelayQueue
Queue 的核心方法分为两组:
| 操作类型 | 抛出异常(不推荐) | 返回特殊值(推荐使用 ⭐) |
|---|---|---|
| 入队(添加元素到队尾) | add(e) |
offer(e)(容量满时返回 false) |
| 出队(移除并返回队首) | remove() |
poll()(队列空时返回 null) |
| 查看(查看但不移除队首) | element() |
peek()(队列空时返回 null) |
强烈建议你在日常开发中使用第二套(返回特殊值) 。因为第一套方法在遇到边界情况(比如队列空了你还要拿数据)时会直接让程序抛出异常崩溃,而第二套会温柔地返回
false或null。
二、为什么 Java 要设计这么多种队列?
队列的核心规则:先进先出 FIFO
但是实际开发中,我们会遇到不同需求:
-
我只是想存东西排队 → 普通队列
-
我希望队列满了就等待 → 阻塞队列
-
我希望优先处理重要任务 → 优先级队列
-
我希望队列容量固定 → 有界队列
-
我希望多线程安全地使用 → 并发队列
-
我希望任务延迟一段时间后才能取 → 延迟队列
-
我甚至不想真正存储任务,只想让两个线程直接交接 → SynchronousQueue
2-1、第一类:普通 Queue
最基础的:
java
Queue<String> queue = new LinkedList<>();
你可以把它理解成:
一个普通的排队结构。
它本身是一个双向链表,不仅实现了
List,也完美实现了Queue接口。
特点:首尾插入和删除速度极快,且没有容量限制(除非内存溢出)。
例如:
java
import java.util.LinkedList;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
// 面向接口编程:左边写接口 Queue,右边写实现类 LinkedList
Queue<String> queue = new LinkedList<>();
// 1. 入队 (offer)
queue.offer("Alice");
queue.offer("Bob");
queue.offer("Charlie");
System.out.println("当前队列: " + queue); // 输出: [Alice, Bob, Charlie]
// 2. 查看队首 (peek)
System.out.println("排在最前面的是: " + queue.peek()); // 输出: Alice
// 3. 出队 (poll)
String served = queue.poll();
System.out.println(served + " 打完饭离开了。");
System.out.println("剩下的队列: " + queue); // 输出: [Bob, Charlie]
}
}
它解决什么问题?
就是:
我需要一个先进先出的数据结构。
比如:
-
任务排队
-
消息排队
-
BFS 广度优先搜索
-
临时保存待处理数据
但是普通 Queue 不保证多线程安全。
LinkedList:为什么它也能当 Queue?
这个地方非常重要。
LinkedList 本身不是专门为 Queue 设计的。
它其实可以同时充当:
List
Queue
Deque
因为它底层是双向链表。
所以:
java
Queue<String> queue = new LinkedList<>();
完全可以。
但是如果你只是需要一个普通队列,现在通常更推荐:
java
Queue<String> queue = new ArrayDeque<>();
因为 ArrayDeque 更专门针对双端队列设计。
2-2、ArrayDeque:高效双端队列(推荐!)
ArrayDeque 基于循环数组实现,是 Java 官方推荐的 Deque 首选实现。
名字拆开:
Array + Deque
Deque = Double Ended Queue
也就是:
两头都可以进,两头都可以出。
比如:
队头 队尾
↓ ↓
A ←→ B ←→ C ←→ D
你可以:
java
deque.offerFirst("A");
deque.offerLast("D");
deque.pollFirst();
deque.pollLast();
所以它不仅可以当:
普通队列
A → B → C
↑
从头取
也可以当:
栈
A
B
C
↑
从顶部进出
所以 ArrayDeque 一个非常重要的特点是:
既可以当 Queue,也可以当 Stack。
示例:
java
import java.util.ArrayDeque;
import java.util.Deque;
public class ArrayDequeDemo {
public static void main(String[] args) {
// 作为普通队列(FIFO)
Deque<String> queue = new ArrayDeque<>();
queue.offer("Task1");
queue.offer("Task2");
// 作为栈使用(LIFO)
Deque<String> stack = new ArrayDeque<>();
stack.push("First");
stack.push("Second");
System.out.println(stack.pop()); // Second(栈顶)
// 双端操作
Deque<Integer> deque = new ArrayDeque<>();
deque.addFirst(1); // 队首入
deque.addLast(2); // 队尾入
System.out.println(deque.removeFirst()); // 1
System.out.println(deque.removeLast()); // 2
}
}
特点:
-
✅ 无容量限制,自动扩容
-
✅ 比
LinkedList更快,缓存友好 -
✅ 内存占用更小
-
❌ 不支持 null 元素
-
❌ 非线程安全
最佳实践 :需要队列/栈时,优先选择 ArrayDeque,而不是 Stack 类或 LinkedList。
2-3、PriorityQueue:为什么突然叫 Priority?
普通 Queue:
A → B → C → D
谁先进去,谁先出来。
但是有时候我们不想这样。
比如医院:
普通病人
普通病人
危重病人
普通病人
你肯定不会:
谁先来的谁先看。
而是:
危重病人
↓
普通病人
↓
普通病人
↓
普通病人
这就是:
优先级队列 Priority Queue
Java:
java
PriorityQueue<Integer> queue = new PriorityQueue<>();
例如:
java
queue.offer(5);
queue.offer(1);
queue.offer(3);
取出来:
1
3
5
所以:
普通 Queue
↓
先进先出
PriorityQueue
↓
优先级高的先出来
它已经不是严格意义上的 FIFO 了。
【示例1】:元素按优先级排序 出队,底层是小顶堆(默认自然顺序)。
java
import java.util.PriorityQueue;
import java.util.Comparator;
public class PriorityQueueDemo {
public static void main(String[] args) {
// 默认小顶堆(数字小的优先)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(5);
minHeap.offer(1);
minHeap.offer(3);
while (!minHeap.isEmpty()) {
System.out.print(minHeap.poll() + " "); // 1 3 5
}
// 大顶堆(自定义比较器)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(
Comparator.reverseOrder()
);
// 对象按自定义字段排序
PriorityQueue<Task> taskQueue = new PriorityQueue<>(
Comparator.comparingInt(t -> t.priority)
);
taskQueue.offer(new Task("紧急Bug", 1));
taskQueue.offer(new Task("写文档", 3));
taskQueue.offer(new Task("重构代码", 2));
}
}
class Task {
String name;
int priority;
Task(String name, int priority) {
this.name = name;
this.priority = priority;
}
}
特点:
-
✅ 插入 O(log n),取队首 O(1)
-
✅ 适合任务调度、TopK 问题
-
❌ 不保证同优先级元素的顺序
-
❌ 非线程安全。
2-4、BlockingQueue:为什么叫 Blocking?
以后学习线程池时非常重要的一个。
普通 Queue:
java
queue.poll();
如果没东西:
队列为空
↓
poll()
↓
返回 null
但是多线程生产者消费者场景中:
生产者
↓
Queue
↓
消费者
如果消费者发现:
Queue 空了
我们可能希望:
"你别返回 null,你先等着,有任务之后再干活。"
这就是:
BlockingQueue
Blocking = 阻塞
例如:
java
BlockingQueue<String> queue =
new ArrayBlockingQueue<>(10);
消费者:
java
String task = queue.take();
如果没有任务:
Queue 空
↓
take()
↓
线程等待
↓
有任务了
↓
继续执行
这就是 BlockingQueue 的核心价值。
2-5、ArrayBlockingQueue:为什么又多了一个 Array?
因为:
BlockingQueue
↓
"阻塞队列"这个概念/接口
下面可以有不同实现。
其中:
ArrayBlockingQueue
就是:
基于数组实现的有界阻塞队列。
这里有三个关键词:
Array
底层使用数组。
Blocking
支持阻塞。
有界
创建的时候就指定容量:
java
new ArrayBlockingQueue<>(10);
最多:
10个元素
如果满了:
Queue 满了
↓
put()
↓
等待
所以它特别适合:
任务数量不能无限堆积的生产者-消费者场景。
2-6、LinkedBlockingQueue------ 线程安全阻塞队列
java
LinkedBlockingQueue
和刚才一样:
Linked
Blocking
Queue
意思就是:
基于链表 + 支持阻塞的队列。
它和 ArrayBlockingQueue 最大的直观区别:
ArrayBlockingQueue
↓
数组
↓
容量通常固定
LinkedBlockingQueue
↓
链表
↓
容量可以指定
例如:
java
new LinkedBlockingQueue<>(100);
可以限制为 100。
也可以:
java
new LinkedBlockingQueue<>();
使用默认容量上限(实际上是非常大的整数上限 Integer.MAX_VALUE)。
示例:
java
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueDemo {
public static void main(String[] args) {
// 有界队列(容量100)
BlockingQueue<String> queue = new LinkedBlockingQueue<>(100);
// 生产者线程
new Thread(() -> {
try {
queue.put("产品A"); // 队列满时阻塞等待
System.out.println("生产: 产品A");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
// 消费者线程
new Thread(() -> {
try {
String product = queue.take(); // 队列空时阻塞等待
System.out.println("消费: " + product);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
核心方法:
| 方法 | 队满/队空时的行为 |
|---|---|
put(e) / take() |
阻塞等待 |
offer(e, time, unit) / poll(time, unit) |
超时等待 |
offer(e) / poll() |
立即返回 false/null |
【注意】:VS ArrayBlockingQueue
Array 和 Linked 指的是这个"队列内部怎么存数据",也就是底层数据结构,不是你往队列里面放的数据类型。
所以 ArrayBlockingQueue 和 LinkedBlockingQueue 都可以存 String、Integer、对象、任务等等。
虽然功能很相似 ,但底层实现不同,会导致一些性能和使用特征上的差异。
最核心的区别可以先这么理解:
| ArrayBlockingQueue | LinkedBlockingQueue | |
|---|---|---|
| 底层 | 数组 | 链表 |
| 是否阻塞 | ✅ | ✅ |
| 是否线程安全 | ✅ | ✅ |
| 是否 FIFO | ✅ | ✅ |
| 是否可以存对象 | ✅ | ✅ |
| 容量 | 必须指定 | 可以指定,也有默认容量 |
| 内存 | 相对紧凑 | 每个节点需要额外对象空间 |
| 适合 | 容量明确、边界清晰 | 任务量可能波动的场景 |
2-7、DelayQueue:为什么叫 Delay?
普通队列:
任务来了
↓
马上可以取
但是有些任务:
必须等到某个时间才能取。
比如:
短信验证码
↓
60秒后过期
或者:
订单
↓
30分钟后检查是否支付
这时候可以用:
DelayQueue
它的特点:
任务没有到期之前,不能被取出来。
比如:
任务A:10:00执行
任务B:10:05执行
任务C:10:10执行
即使任务 C 先进入队列:
C
B
A
也不是说谁先进入谁先执行。
而是:
到期时间最早
↓
A
↓
B
↓
C
所以:
DelayQueue
↓
"时间到了才能取"
核心特点:
| 特性 | 说明 |
|---|---|
| 元素要求 | 元素必须实现 Delayed 接口 |
| 出队规则 | 只有到期(delay ≤ 0) 的元素才能被 take() 取出 |
| 队首 | 永远是最早到期的元素 |
| 容量 | 无界队列,理论上无限大 |
| 线程安全 | 线程安全,支持阻塞等待 |
2-8、SynchronousQueue:这个名字特别容易懵
它和普通队列最大的区别是:
它几乎不存东西。
你可以把它理解成:
生产者 ─────────→ 消费者
不是:
生产者
↓
┌───────────┐
│ Queue │
│ A B C D │
└───────────┘
↓
消费者
而是:
生产者
↓
[A] ←→ 消费者
生产者:
java
queue.put("A");
必须等到有消费者:
java
queue.take();
两边才能完成交接。
所以叫:
Synchronous:同步交接
它非常适合:
任务不需要在队列里堆积,生产者和消费者直接交接。
三、把这些队列放到一张图里
Queue
│
┌─────────────┼─────────────┐
│ │ │
普通队列 优先级队列 阻塞队列
│ │ │
ArrayDeque PriorityQueue │
LinkedList │
┌──────────┼───────────┐
│ │ │
ArrayBlockingQueue │ SynchronousQueue
│
LinkedBlockingQueue
│
DelayQueue
不过这里有一个很重要的概念纠正:
ArrayDeque、PriorityQueue、BlockingQueue 并不是简单的"父子分类关系"。更准确地说,是不同接口/实现针对不同需求组合出来的设计。
四、小结
① 我要普通 FIFO?
ArrayDeque
② 我要两头都能操作?
Deque
↓
ArrayDeque
③ 我要按照优先级取?
PriorityQueue
④ 我要多线程 + 队列空了就等待?
BlockingQueue
⑤ 我要固定容量?
ArrayBlockingQueue
⑥ 我要链表实现的阻塞队列?
LinkedBlockingQueue
⑦ 我要"到时间才能取"?
DelayQueue
⑧ 我要生产者和消费者直接交接,不在队列里存任务?
SynchronousQueue