【Java从入门到入土】24:Queue队列:从排队到优先处理
【Java从入门到入土】24:Queue队列:从排队到优先处理
Queue(队列)是Java集合框架中处理“有序序列”的核心容器,它的设计哲学是“按规则存取元素”——从最基础的FIFO(先进先出)队列,到支持优先级的PriorityQueue,再到线程安全的阻塞队列,队列几乎贯穿了Java并发编程的所有场景(如线程池、消息队列)。新手常问:“队列和List有什么区别?阻塞队列为什么能支撑线程池?PriorityQueue的排序原理是什么?” 今天从队列基础、核心实现、并发队列、实战应用四个维度,把队列的“排队逻辑”讲透,让你理解从“普通排队”到“优先处理”的完整实现。
📚 Queue基础:FIFO与优先级队列
Queue是一个接口,定义了“队列”的核心行为,它的实现类分为两大方向:普通队列(FIFO) 和优先级队列(按优先级排序),前者遵循“先来先服务”,后者打破顺序,优先处理高优先级元素。
1. Queue接口核心方法
Queue定义了两套操作方法(抛异常 vs 返回特殊值),适配不同的错误处理场景:
| 操作 | 抛异常(失败时) | 返回特殊值(失败时) | 说明 |
|---|---|---|---|
| 添加元素到队尾 | add(e) | offer(e) | 队列满时,add抛IllegalStateException,offer返回false |
| 取出队首元素 | remove() | poll() | 队列空时,remove抛NoSuchElementException,poll返回null |
| 查看队首元素 | element() | peek() | 队列空时,element抛NoSuchElementException,peek返回null |
基础用法示例:
import java.util.LinkedList;
import java.util.Queue;
public class QueueBasicTest {
public static void main(String[] args) {
// LinkedList实现了Queue接口,是最常用的非阻塞FIFO队列
Queue<String> queue = new LinkedList<>();
// 1. 添加元素(offer更安全,推荐)
queue.offer("任务1");
queue.offer("任务2");
queue.offer("任务3");
System.out.println("队列初始状态:" + queue); // [任务1, 任务2, 任务3]
// 2. 查看队首(不取出)
System.out.println("队首元素:" + queue.peek()); // 任务1
// 3. 取出队首(FIFO,先进先出)
while (!queue.isEmpty()) {
String task = queue.poll();
System.out.println("处理任务:" + task); // 依次输出:任务1→任务2→任务3
}
// 4. 空队列poll返回null(安全)
System.out.println("空队列poll:" + queue.poll()); // null
}
}
2. 普通队列 vs 优先级队列
| 类型 | 核心规则 | 典型实现类 | 适用场景 |
|---|---|---|---|
| 普通队列(FIFO) | 先进先出,按插入顺序处理 | LinkedList、ArrayDeque | 普通任务排队、消息转发 |
| 优先级队列 | 按优先级排序,高优先级先处理 | PriorityQueue | 任务调度、事件优先级处理 |
🚦 阻塞队列:ArrayBlockingQueue与LinkedBlockingQueue
阻塞队列(BlockingQueue)是并发编程的核心工具,它在普通队列基础上增加了“阻塞”特性:
- 入队阻塞:队列满时,入队线程阻塞,直到队列有空位;
- 出队阻塞:队列空时,出队线程阻塞,直到队列有元素。
阻塞队列是线程池、生产者-消费者模型的核心依赖,JDK提供了多个实现,其中ArrayBlockingQueue和LinkedBlockingQueue是最常用的两个。
1. 核心阻塞方法
BlockingQueue扩展了Queue接口,新增4个核心阻塞方法:
| 操作 | 阻塞版本(等待) | 超时版本(等待指定时间) |
|---|---|---|
| 入队 | put(e) | offer(e, timeout, unit) |
| 出队 | take() | poll(timeout, unit) |
2. ArrayBlockingQueue:数组实现的有界阻塞队列
(1)核心特性
- 底层:定长数组,创建时必须指定容量(有界);
- 锁机制:单锁(ReentrantLock),入队/出队共用一把锁,并发度较低;
- 公平性:支持公平/非公平锁(公平锁按线程等待顺序访问,性能略低);
- 适用场景:固定容量、数据量可预估的场景(如固定大小的消息队列)。
(2)用法示例(生产者-消费者)
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ArrayBlockingQueueTest {
// 容量为5的有界阻塞队列
private static final BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
public static void main(String[] args) {
// 生产者线程:生产消息
new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
String msg = "消息" + i;
queue.put(msg); // 队列满时阻塞
System.out.println("生产者生产:" + msg + ",队列大小:" + queue.size());
Thread.sleep(100); // 模拟生产耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "生产者").start();
// 消费者线程:消费消息
new Thread(() -> {
try {
while (true) {
String msg = queue.take(); // 队列空时阻塞
System.out.println("消费者消费:" + msg + ",队列大小:" + queue.size());
Thread.sleep(500); // 模拟消费耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "消费者").start();
}
}
3. LinkedBlockingQueue:链表实现的阻塞队列
(1)核心特性
- 底层:双向链表,默认容量
Integer.MAX_VALUE(可认为无界,也可指定容量); - 锁机制:双锁(ReentrantLock),入队锁和出队锁分离,并发度更高;
- 适用场景:高并发、数据量不可预估的场景(如线程池的任务队列)。
(2)核心对比(ArrayBlockingQueue vs LinkedBlockingQueue)
| 特性 | ArrayBlockingQueue | LinkedBlockingQueue |
|---|---|---|
| 底层结构 | 定长数组 | 双向链表 |
| 容量 | 必须指定(有界) | 可选(默认无界) |
| 锁机制 | 单锁(入队/出队互斥) | 双锁(入队/出队并行) |
| 内存占用 | 连续内存,无冗余 | 非连续内存,节点有额外开销 |
| 并发性能 | 较低 | 较高 |
| 适用场景 | 固定容量、低并发 | 动态容量、高并发 |
4. 阻塞队列核心应用:生产者-消费者模型
阻塞队列完美解决了生产者和消费者的“同步问题”——无需手动加锁/等待,底层通过Condition实现阻塞/唤醒:
🔄 双端队列Deque:栈与队列的结合体
Deque(Double Ended Queue)是“双端队列”,支持在队首和队尾同时进行入队/出队操作,既能当FIFO队列用,也能当LIFO栈用,是比Queue更灵活的容器。
1. Deque核心方法
Deque整合了队列和栈的操作,核心方法分类如下:
| 操作方向 | 队列(FIFO) | 栈(LIFO) |
|---|---|---|
| 入队/入栈 | offerLast(e) / addLast(e) | offerFirst(e) / push(e) |
| 出队/出栈 | pollFirst() / removeFirst() | pollLast() / pop() |
| 查看首/顶 | peekFirst() / getFirst() | peekLast() / peek() |
2. 核心实现类:ArrayDeque
ArrayDeque是Deque的首选实现类,底层是可扩容的循环数组,性能优于LinkedList(栈/队列场景):
- 无容量限制(自动扩容),非线程安全;
- 循环数组避免了普通数组的元素移动,首尾操作O(1);
- 不允许存储null元素。
用法示例(栈+队列)
import java.util.ArrayDeque;
import java.util.Deque;
public class DequeTest {
public static void main(String[] args) {
Deque<String> deque = new ArrayDeque<>();
// 1. 作为FIFO队列使用
deque.offerLast("队列元素1");
deque.offerLast("队列元素2");
System.out.println("队列出队:" + deque.pollFirst()); // 队列元素1
// 2. 作为LIFO栈使用(替代Stack,Stack是老旧类)
deque.push("栈元素1");
deque.push("栈元素2");
System.out.println("栈出栈:" + deque.pop()); // 栈元素2
// 3. 双端操作
deque.offerFirst("队首元素");
deque.offerLast("队尾元素");
System.out.println("队首取出:" + deque.pollFirst()); // 队首元素
System.out.println("队尾取出:" + deque.pollLast()); // 队尾元素
}
}
3. Deque vs Stack
Java中的Stack类是老旧实现(继承Vector),性能低且线程安全(无意义),官方推荐用Deque替代Stack:
| 特性 | Stack(不推荐) | Deque(ArrayDeque) |
|---|---|---|
| 底层结构 | 数组(Vector子类) | 循环数组 |
| 线程安全 | 是(synchronized) | 否 |
| 性能 | 低 | 高 |
| 支持操作 | 仅栈操作 | 栈+队列+双端操作 |
| 空值 | 允许 | 不允许 |
🌿 优先级队列PriorityQueue:堆的实现
PriorityQueue是基于二叉堆实现的优先级队列,核心特点是“元素按优先级排序,而非插入顺序”——每次取出的都是优先级最高的元素(堆顶)。
1. 底层原理:二叉堆
二叉堆是完全二叉树的一种,分为“最小堆”和“最大堆”,PriorityQueue默认是最小堆(小值优先):
- 最小堆:每个父节点的值 ≤ 子节点的值,堆顶是最小值;
- 堆的操作(插入/删除)时间复杂度O(logn),查看堆顶O(1);
- 底层用数组存储堆结构,通过索引计算父子节点位置:
- 父节点索引:
(i - 1) / 2 - 左子节点索引:
2 * i + 1 - 右子节点索引:
2 * i + 2
- 父节点索引:
2. 核心用法
(1)自然排序(元素实现Comparable)
import java.util.PriorityQueue;
import java.util.Queue;
// 任务类,按优先级(数字越小优先级越高)排序
class Task implements Comparable<Task> {
private int priority; // 优先级
private String name;
public Task(int priority, String name) {
this.priority = priority;
this.name = name;
}
// 最小堆:优先级小的排前面
@Override
public int compareTo(Task o) {
return this.priority - o.priority;
}
@Override
public String toString() {
return "Task{priority=" + priority + ", name='" + name + "'}";
}
}
public class PriorityQueueNaturalTest {
public static void main(String[] args) {
Queue<Task> queue = new PriorityQueue<>();
// 添加任务(优先级:1>3>5)
queue.offer(new Task(5, "普通任务"));
queue.offer(new Task(1, "紧急任务"));
queue.offer(new Task(3, "重要任务"));
// 按优先级取出(紧急→重要→普通)
while (!queue.isEmpty()) {
System.out.println("处理任务:" + queue.poll());
}
// 输出:
// 处理任务:Task{priority=1, name='紧急任务'}
// 处理任务:Task{priority=3, name='重要任务'}
// 处理任务:Task{priority=5, name='普通任务'}
}
}
(2)自定义排序(最大堆)
import java.util.Comparator;
import java.util.PriorityQueue;
public class PriorityQueueCustomTest {
public static void main(String[] args) {
// 自定义比较器:最大堆(数字越大优先级越高)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
maxHeap.offer(3);
maxHeap.offer(1);
maxHeap.offer(5);
// 按优先级取出(5→3→1)
while (!maxHeap.isEmpty()) {
System.out.println("取出元素:" + maxHeap.poll());
}
}
}
3. 核心特性总结
| 特性 | PriorityQueue |
|---|---|
| 底层结构 | 二叉堆(数组实现) |
| 排序规则 | 默认最小堆,支持自定义Comparator |
| 线程安全 | 非线程安全 |
| 允许null | 不允许 |
| 时间复杂度 | 插入/删除:O(logn),查看堆顶:O(1) |
| 适用场景 | 任务调度、事件优先级处理、TopN问题 |
4. 经典应用:TopN问题
用PriorityQueue实现“找出数组中最大的3个数”:
import java.util.Comparator;
import java.util.PriorityQueue;
public class TopNTest {
public static void main(String[] args) {
int[] nums = {5, 2, 9, 1, 7, 3, 8};
int n = 3;
// 用最小堆实现TopN(堆大小固定为n,效率更高)
PriorityQueue<Integer> minHeap = new PriorityQueue<>(n);
for (int num : nums) {
if (minHeap.size() < n) {
minHeap.offer(num);
} else if (num > minHeap.peek()) {
minHeap.poll();
minHeap.offer(num);
}
}
System.out.println("最大的" + n + "个数:" + minHeap); // [7, 8, 9]
}
}
🎯 队列在线程池中的应用
线程池是队列最核心的实战场景,线程池的“任务队列”直接决定了线程池的行为,不同队列适配不同的线程池策略:
1. 线程池与队列的关系
2. 线程池常用队列及适配策略
| 队列类型 | 线程池策略 | 适用场景 |
|---|---|---|
| ArrayBlockingQueue(有界) | 固定核心线程数+有限最大线程数 | 资源可控的场景(如核心业务线程池) |
| LinkedBlockingQueue(无界) | 固定核心线程数(最大=核心) | 任务量不可预估,需避免拒绝任务 |
| SynchronousQueue(同步队列) | 缓存线程池(核心=0,最大=Integer.MAX_VALUE) | 任务快速处理,无队列等待 |
| PriorityBlockingQueue(优先级) | 自定义线程池 | 需按优先级处理任务的场景 |
3. 实战示例:自定义线程池(ArrayBlockingQueue)
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolQueueTest {
public static void main(String[] args) {
// 自定义线程池:核心线程2,最大线程4,队列容量3
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60, TimeUnit.SECONDS, // 非核心线程空闲超时
new ArrayBlockingQueue<>(3), // 有界任务队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用者执行
);
// 提交8个任务,观察队列和线程的处理逻辑
for (int i = 1; i <= 8; i++) {
int taskId = i;
executor.submit(() -> {
try {
System.out.println("任务" + taskId + "由线程" + Thread.currentThread().getName() + "处理");
Thread.sleep(1000); // 模拟任务耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
executor.shutdown();
}
}
执行逻辑分析:
- 任务1、2:核心线程处理;
- 任务3、4、5:加入队列等待;
- 任务6、7:创建非核心线程处理(最大线程4);
- 任务8:队列满+最大线程满,执行拒绝策略(调用者线程处理)。
📌 核心总结
队列的核心是“按规则存取元素”,不同队列的选择关键在于“顺序、并发、优先级”,核心要点如下:
- 基础队列:LinkedList实现Queue,是普通FIFO队列的首选,Deque(ArrayDeque)替代Stack,支持双端操作;
- 阻塞队列:ArrayBlockingQueue(有界、单锁)适用于固定容量场景,LinkedBlockingQueue(无界、双锁)适用于高并发场景,是生产者-消费者模型的核心;
- 优先级队列:PriorityQueue基于二叉堆实现,支持按优先级存取,适用于任务调度、TopN问题;
- 线程池应用:有界队列(ArrayBlockingQueue)控制资源,无界队列(LinkedBlockingQueue)避免拒绝任务,同步队列(SynchronousQueue)快速处理任务;
- 选型原则:普通场景用LinkedList/ArrayDeque,并发场景用ArrayBlockingQueue/LinkedBlockingQueue,优先级场景用PriorityQueue。
掌握这些,你就能根据业务场景选择最合适的队列实现类,无论是普通的任务排队,还是高并发的线程池设计,都能精准匹配需求——真正理解队列“从排队到优先处理”的设计精髓。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)