Java 实例 – 队列(Queue)用法(长文解析)
💡一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...
,点击查看项目介绍 ;演示链接: http://116.62.199.48:7070 ;- 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;
截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观
前言
在编程世界中,队列(Queue)是一种常见的数据结构,它遵循“先进先出”(FIFO, First-In-First-Out)的原则。想象你站在电影院的售票窗口前,前面的人排成一列,每个人按顺序购票——这就是队列的经典应用场景。在 Java 中,队列通过 java.util.Queue
接口及其多种实现类(如 LinkedList
、ArrayDeque
、PriorityQueue
等)提供丰富的功能。本文将从基础概念、核心方法、实现类差异、线程安全队列到实际案例,逐步解析 Java 队列的用法,并结合代码示例帮助读者深入理解。
一、队列(Queue)的核心概念与接口
1.1 什么是队列?
队列是一种线性数据结构,其核心特点是“先进先出”。例如,打印机的任务队列、银行的客户等待队列,都符合这一特性。在 Java 中,队列通过 Queue
接口定义,该接口继承自 Collection
接口,因此具备集合的基本操作能力。
1.2 关键方法解析
以下方法是 Queue
接口的核心操作:
add(E e)
:将元素插入队列尾部,若队列已满会抛出异常(具体取决于实现类)。offer(E e)
:与add
类似,但队列满时返回false
而非抛出异常。remove()
:移除并返回队列头部的元素,若队列为空则抛出NoSuchElementException
。poll()
:移除并返回队列头部的元素,若队列为空则返回null
。element()
:返回队列头部元素但不移除,若队列为空则抛出异常。peek()
:与element
类似,但队列为空时返回null
。
比喻说明:
add/remove/element
类似“强硬”的操作,直接执行动作但可能引发错误;offer/poll/peek
则像“礼貌询问”,在队列为空时返回友好结果而非强制报错。
二、常用队列实现类详解
Java 提供了多个 Queue
的实现类,每个类在性能、线程安全性和使用场景上有所差异。以下是几个核心实现的对比与示例:
2.1 LinkedList
:灵活的双端队列
LinkedList
是 Queue
接口的常见实现,它同时实现了 Deque
(双端队列)接口。虽然 LinkedList
的插入和删除操作在链表中间效率较低,但在队列头部和尾部操作(如 addFirst
、removeLast
)时表现良好。
示例代码:
Queue<String> linkedQueue = new LinkedList<>();
linkedQueue.offer("任务1");
linkedQueue.offer("任务2");
System.out.println(linkedQueue.poll()); // 输出 "任务1"
System.out.println(linkedQueue.peek()); // 输出 "任务2"
2.2 ArrayDeque
:高效的数组实现
ArrayDeque
是基于数组的队列实现,其名称来源于“Array”和“Deque”的组合。它在头部和尾部的插入、删除操作均为 O(1) 时间复杂度,性能优于 LinkedList
。但 ArrayDeque
不支持 null
元素,需注意使用场景。
示例代码:
Queue<Integer> arrayQueue = new ArrayDeque<>();
arrayQueue.add(100);
arrayQueue.add(200);
System.out.println(arrayQueue.remove()); // 输出 100
System.out.println(arrayQueue.size()); // 输出 1
2.3 PriorityQueue
:按优先级排序的队列
PriorityQueue
的特殊之处在于元素按优先级(自然顺序或自定义比较器)排序,头部始终是最小(或最大)的元素。例如,医院急诊室的“危重患者优先”规则,可以用 PriorityQueue
实现。
示例代码(自然排序):
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
priorityQueue.offer(50);
priorityQueue.offer(30);
priorityQueue.offer(70);
System.out.println(priorityQueue.poll()); // 输出 30(最小值)
示例代码(自定义比较器):
// 按字符串长度降序排列
Comparator<String> comparator = (s1, s2) -> Integer.compare(s2.length(), s1.length());
PriorityQueue<String> customQueue = new PriorityQueue<>(comparator);
customQueue.add("apple");
customQueue.add("banana");
customQueue.add("kiwi");
System.out.println(customQueue.poll()); // 输出 "banana"(长度最长)
三、线程安全队列与并发场景
在多线程环境下,普通队列(如 LinkedList
、ArrayDeque
)可能因并发操作导致数据不一致或异常。Java 提供了线程安全的队列实现,如 ConcurrentLinkedQueue
和 BlockingQueue
。
3.1 ConcurrentLinkedQueue
:无阻塞线程安全队列
ConcurrentLinkedQueue
是基于链表的无界线程安全队列,通过原子操作(CAS)实现高效并发访问。它不提供阻塞方法,适合“生产者-消费者”模式中无需等待的场景。
示例代码:
ConcurrentLinkedQueue<String> concurrentQueue = new ConcurrentLinkedQueue<>();
concurrentQueue.add("线程安全任务");
System.out.println(concurrentQueue.poll()); // 输出 "线程安全任务"
3.2 BlockingQueue
:带阻塞功能的队列
BlockingQueue
接口(如 LinkedBlockingQueue
、ArrayBlockingQueue
)在队列为空或满时会阻塞线程,直到满足条件。这是实现生产者-消费者模式的首选工具。
示例代码(生产者-消费者模式):
// 定义一个容量为3的阻塞队列
BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>(3);
// 生产者线程
new Thread(() -> {
try {
blockingQueue.put("任务A");
blockingQueue.put("任务B");
blockingQueue.put("任务C");
blockingQueue.put("任务D"); // 此时队列已满,线程阻塞
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
// 消费者线程
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1); // 模拟延迟
System.out.println(blockingQueue.take()); // 输出 "任务A"
System.out.println(blockingQueue.take()); // 输出 "任务B"
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
四、实战案例:任务调度系统
4.1 场景描述
假设我们需要开发一个简单的任务调度系统,要求:
- 支持动态添加任务;
- 任务按顺序执行;
- 允许多个消费者(线程)同时处理任务。
4.2 实现思路
使用 ArrayBlockingQueue
作为任务队列,结合线程池实现消费者线程的并行处理。
完整代码示例:
import java.util.concurrent.*;
// 定义任务类
class Task {
private String name;
public Task(String name) { this.name = name; }
public void execute() {
System.out.println(Thread.currentThread().getName() + " 正在执行任务:" + name);
}
}
public class TaskScheduler {
private static final int QUEUE_CAPACITY = 5;
private static BlockingQueue<Task> taskQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY);
private static ExecutorService executor = Executors.newFixedThreadPool(2);
// 生产者方法:添加任务到队列
public static void addTask(Task task) {
try {
taskQueue.put(task);
System.out.println("任务 " + task.name + " 已加入队列");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 消费者线程:从队列获取任务并执行
private static class TaskConsumer implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
Task task = taskQueue.take();
task.execute();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
public static void main(String[] args) {
// 启动消费者线程
executor.submit(new TaskConsumer());
executor.submit(new TaskConsumer());
// 模拟生产任务
for (int i = 1; i <= 7; i++) {
addTask(new Task("任务" + i));
}
// 关闭线程池
executor.shutdown();
try {
executor.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果示例:
任务 任务1 已加入队列
任务 任务2 已加入队列
任务 任务3 已加入队列
任务 任务4 已加入队列
任务 任务5 已加入队列
任务 任务6 已加入队列
任务 任务7 已加入队列
pool-1-thread-1 正在执行任务:任务1
pool-1-thread-2 正在执行任务:任务2
pool-1-thread-1 正在执行任务:任务3
pool-1-thread-2 正在执行任务:任务4
pool-1-thread-1 正在执行任务:任务5
pool-1-thread-2 正在执行任务:任务6
pool-1-thread-1 正在执行任务:任务7
结论
通过本文的讲解,读者应能掌握 Java 队列的基本概念、核心方法、实现类差异以及线程安全场景的解决方案。队列不仅是数据结构的基础工具,更是构建高并发系统的关键组件。建议读者通过实际项目(如任务调度、消息队列)进一步练习,以加深对队列的理解。
关键词自然布局:本文通过代码示例、场景分析和对比表格,深入探讨了“Java 实例 – 队列(Queue)用法”,帮助开发者选择最适合的实现类并解决实际问题。