Java 队列

在 Java 并发编程与数据结构领域,队列(Queue)作为一种遵循 FIFO(First-In-First-Out,先进先出)原则的线性数据结构,扮演着至关重要的角色。无论是任务调度、消息中间件底层实现,还是高并发场景下的流量削峰,队列都发挥着不可替代的作用。

一、Java 队列基础:概念与核心接口

1.1 队列的定义与特性

队列是一种特殊的线性表,它仅允许在表的一端(队尾,Rear)进行插入操作(入队,enqueue),在另一端(队头,Front)进行删除操作(出队,dequeue)。这种 "先进先出" 的特性,使其与栈(Stack,LIFO)形成鲜明对比。

在实际应用中,队列还衍生出两种常见变体:

  • 双端队列(Deque):允许在队头和队尾同时进行插入和删除操作,兼具队列与栈的特性;
  • 优先级队列(PriorityQueue):不遵循 FIFO 原则,而是根据元素的优先级排序,每次出队的是优先级最高的元素。

1.2 Queue 接口核心方法

Java 集合框架中,java.util.Queue接口定义了队列的核心操作,其方法可分为两类(避免抛出异常 vs 返回特殊值),具体如下表所示:

|------|--------------|---------------|
| 操作类型 | 抛出异常(当操作失败时) | 返回特殊值(当操作失败时) |
| 入队 | add(E e) | offer(E e) |
| 出队 | remove() | poll() |
| 查看队头 | element() | peek() |

注意:add()和remove()在操作失败时(如队列已满 / 为空)会抛出IllegalStateException或NoSuchElementException,而offer()和poll()会返回false或null,更适合高并发场景下的优雅处理。

二、Java 队列核心实现类解析

Java 中 Queue 接口的实现类可分为非并发队列 (如 PriorityQueue、LinkedList)和并发队列(如 ConcurrentLinkedQueue、ArrayBlockingQueue),下面逐一剖析其原理与应用场景。

2.1 非并发队列:PriorityQueue 与 LinkedList

2.1.1 PriorityQueue:优先级队列

PriorityQueue 是基于二叉堆(默认小顶堆)实现的优先级队列,它不遵循 FIFO 原则,而是根据元素的自然顺序或自定义比较器(Comparator)排序。

核心特性

  • 底层存储:动态数组(初始容量 11,扩容时若当前容量 < 64 则翻倍,否则增加 50%);
  • 排序规则:默认按元素自然顺序升序排列,也可通过构造函数传入 Comparator 自定义排序;
  • 线程安全性:非线程安全,不支持并发操作;
  • 禁止 null 元素:插入 null 会抛出 NullPointerException。

源码关键逻辑(以 offer () 为例)

java 复制代码
public boolean offer(E e) {

if (e == null)

throw new NullPointerException();

modCount++;

int i = size;

if (i >= queue.length)

grow(i + 1); // 扩容

size = i + 1;

if (i == 0)

queue[0] = e; // 第一个元素直接插入

else

siftUp(i, e); // 调整堆结构,维持小顶堆特性

return true;

}

// 向上调整堆

private void siftUp(int k, E x) {

if (comparator != null)

siftUpUsingComparator(k, x);

else

siftUpComparable(k, x);

}

应用场景:任务调度(如线程池中的延迟任务队列 ScheduledThreadPoolExecutor)、TopK 问题求解等。

2.1.2 LinkedList:基于链表的队列

LinkedList 实现了 Queue 接口,本质是一个双向链表,可作为普通队列(FIFO)或双端队列(Deque)使用。

核心特性

  • 底层存储:双向链表,每个节点包含 prev、next 指针和元素值;
  • 线程安全性:非线程安全,并发操作需手动加锁(如 synchronized);
  • 性能特点:入队(offer ())和出队(poll ())操作均为 O (1) 时间复杂度,优于 ArrayList(数组尾部插入为 O (1),头部删除为 O (n))。

应用场景:适合频繁进行插入和删除操作的场景,如实现消息队列的基础结构(非并发场景)。

2.2 并发队列:线程安全的队列实现

在高并发场景下,非并发队列会出现线程安全问题(如元素丢失、队列结构破坏),因此 Java 提供了多种线程安全的并发队列,主要分为阻塞队列非阻塞队列两类。

2.2.1 非阻塞队列:ConcurrentLinkedQueue

ConcurrentLinkedQueue 是基于无锁 CAS(Compare-and-Swap) 实现的非阻塞并发队列,底层采用单向链表结构,支持高并发环境下的高效读写操作。

核心特性

  • 线程安全性:通过 CAS 操作保证原子性,无需加锁,性能优于阻塞队列;
  • 无界队列:理论上无容量限制(受内存大小限制),不会出现队列满的情况;
  • 性能特点:入队(offer ())和出队(poll ())操作均为 O (1) 时间复杂度,支持多线程同时读写;
  • 不支持 null 元素:插入 null 会抛出 NullPointerException。

CAS 核心逻辑(以入队为例)

java 复制代码
public boolean offer(E e) {

checkNotNull(e);

final Node<E> newNode = new Node<E>(e);

// 从队尾开始,通过CAS循环尝试将新节点插入

for (Node<E> t = tail, p = t;;) {

Node<E> q = p.next;

if (q == null) {

// p是队尾,尝试CAS将newNode设为p的next

if (p.casNext(null, newNode)) {

// 如果p不是tail,更新tail为newNode

if (p != t)

casTail(t, newNode);

return true;

}

}

// 如果q不是null,p可能不是队尾,重新定位p

else if (p == q)

p = (t != (t = tail)) ? t : head;

else

p = (p != t && t != (t = tail)) ? t : q;

}

}

应用场景:高并发场景下的无界消息传递,如日志收集、数据同步等。

2.2.2 阻塞队列:ArrayBlockingQueue 与 LinkedBlockingQueue

阻塞队列(BlockingQueue)是并发队列的重要分支,它在 Queue 接口基础上增加了阻塞特性:当队列满时,入队操作会阻塞;当队列空时,出队操作会阻塞。这种特性使其非常适合实现 "生产者 - 消费者" 模式。

Java 中常用的阻塞队列有ArrayBlockingQueue(基于数组)和LinkedBlockingQueue(基于链表),两者的对比如下表所示:

|------|---------------------------|---------------------------------|
| 特性 | ArrayBlockingQueue | LinkedBlockingQueue |
| 底层存储 | 固定大小的数组(初始化时需指定容量) | 双向链表(默认无界,也可指定容量) |
| 容量特性 | 有界队列(容量不可变) | 可选有界 / 无界(默认 Integer.MAX_VALUE) |
| 锁机制 | 单锁(ReentrantLock)+ 两个条件变量 | 双锁(takeLock 和 putLock)+ 各自条件变量 |
| 性能 | 读写操作竞争同一把锁,高并发下性能略低 | 读写操作使用不同锁,高并发下性能更优 |
| 内存占用 | 数组预分配内存,内存占用稳定 | 链表节点动态创建,内存占用随元素数量变化 |

核心方法(阻塞队列特有)

  • put(E e):入队,若队列满则阻塞,直到队列有空闲空间;
  • take():出队,若队列为空则阻塞,直到队列有元素;
  • offer(E e, long timeout, TimeUnit unit):入队,若队列满则阻塞指定时间,超时后返回 false;
  • poll(long timeout, TimeUnit unit):出队,若队列为空则阻塞指定时间,超时后返回 null。

应用场景

  • ArrayBlockingQueue:适合对队列容量有明确限制的场景,如线程池中的任务队列(FixedThreadPool 默认使用);
  • LinkedBlockingQueue:适合队列容量不确定、高并发读写的场景,如消息中间件的消息存储队列。

三、实战案例:基于阻塞队列实现生产者 - 消费者模式

生产者 - 消费者模式是并发编程中的经典模式,它通过队列实现生产者线程与消费者线程的解耦,平衡两者的处理速度。下面基于ArrayBlockingQueue实现一个简单的生产者 - 消费者案例。

3.1 案例需求

  • 1 个生产者线程:每秒生产 1 个 "产品"(随机字符串),并放入队列;
  • 2 个消费者线程:从队列中获取产品,模拟 "消费"(打印产品信息);
  • 队列容量限制为 5,当队列满时生产者阻塞,当队列空时消费者阻塞。

3.2 代码实现

java 复制代码
import java.util.Random;

import java.util.concurrent.ArrayBlockingQueue;

import java.util.concurrent.BlockingQueue;

public class ProducerConsumerDemo {

// 队列容量

private static final int QUEUE_CAPACITY = 5;

// 产品数量(生产10个后停止)

private static final int PRODUCT_COUNT = 10;

// 阻塞队列

private static final BlockingQueue<String> queue = new ArrayBlockingQueue<>(QUEUE_CAPACITY);

// 生产者线程

static class Producer implements Runnable {

@Override

public void run() {

Random random = new Random();

for (int i = 1; i <= PRODUCT_COUNT; i++) {

try {

String product = "Product-" + i + "-" + random.nextInt(1000);

queue.put(product); // 入队,队列满则阻塞

System.out.println(Thread.currentThread().getName() + " 生产产品:" + product + ",当前队列大小:" + queue.size());

Thread.sleep(1000); // 模拟生产耗时

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

}

}

// 生产结束,添加"结束标记"(消费者线程需处理)

try {

queue.put("PRODUCE_FINISH");

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

}

System.out.println(Thread.currentThread().getName() + " 生产结束!");

}

}

// 消费者线程

static class Consumer implements Runnable {

@Override

public void run() {

while (true) {

try {

String product = queue.take(); // 出队,队列空则阻塞

// 判断是否为结束标记

if ("PRODUCE_FINISH".equals(product)) {

// 将结束标记放回队列,供其他消费者线程识别

queue.put(product);

System.out.println(Thread.currentThread().getName() + " 消费结束!");

break;

}

System.out.println(Thread.currentThread().getName() + " 消费产品:" + product + ",当前队列大小:" + queue.size());

Thread.sleep(1500); // 模拟消费耗时(消费比生产慢,队列会逐渐满)

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

}

}

}

}

public static void main(String[] args) {

// 启动1个生产者线程

new Thread(new Producer(), "Producer-1").start();

// 启动2个消费者线程

new Thread(new Consumer(), "Consumer-1").start();

new Thread(new Consumer(), "Consumer-2").start();

}

}

3.3 运行结果分析

运行代码后,可观察到以下现象:

  1. 生产者每秒生产 1 个产品,队列大小逐渐增加;
  1. 消费者每 1.5 秒消费 1 个产品,由于消费速度慢于生产速度,队列会逐渐满至 5;
  1. 当队列满时,生产者线程阻塞,直到消费者消费 1 个产品后,生产者才继续生产;
  1. 当生产者生产完 10 个产品后,添加 "PRODUCE_FINISH" 标记,消费者线程识别到标记后停止运行。

该案例充分体现了阻塞队列的核心价值:无需手动处理线程间的同步与通信,通过队列的阻塞特性即可实现生产者与消费者的协调。

四、Java 队列性能对比与选型建议

4.1 核心队列性能对比(基于 JMH 基准测试)

为了更直观地了解不同队列的性能,我们通过 JMH(Java Microbenchmark Harness)测试了常见队列的入队(offer ())和出队(poll ())性能(测试环境:JDK 17,8 核 CPU,16GB 内存)。

|-----------------------|-------|---------------|---------------|----------------|
| 队列类型 | 并发线程数 | 入队 QPS(万 / 秒) | 出队 QPS(万 / 秒) | 适用场景 |
| PriorityQueue | 1 | 38.2 | 35.6 | 单线程优先级排序 |
| LinkedList | 1 | 42.5 | 40.1 | 单线程频繁插入删除 |
| ConcurrentLinkedQueue | 8 | 125.3 | 118.7 | 高并发无界场景 |
| ArrayBlockingQueue | 8 | 98.6 | 92.3 | 高并发有界场景 |
| LinkedBlockingQueue | 8 | 112.4 | 105.8 | 高并发无界 / 大吞吐量场景 |

结论

  • 单线程场景:LinkedList 性能略优于 PriorityQueue;
  • 高并发场景:非阻塞队列(ConcurrentLinkedQueue)性能优于阻塞队列;
  • 阻塞队列中:LinkedBlockingQueue 因双锁机制,性能优于 ArrayBlockingQueue。

4.2 队列选型建议

在实际开发中,队列的选型需结合业务场景的核心需求,以下是具体建议:

  1. 单线程 / 低并发场景
    • 若需优先级排序:选择PriorityQueue;
    • 若需频繁插入删除:选择LinkedList。
  1. 高并发场景
    • 无界队列需求(如日志收集):选择ConcurrentLinkedQueue;
    • 有界队列需求(如流量控制):选择ArrayBlockingQueue;
    • 大吞吐量需求(如消息中间件):选择LinkedBlockingQueue;
    • 延迟队列需求(如定时任务):选择DelayQueue。
  1. 特殊场景
    • 线程池任务队列:根据线程池类型选择(如 FixedThreadPool 用 LinkedBlockingQueue,ScheduledThreadPool 用 DelayedWorkQueue);
    • 分布式场景:需使用 Redis、RabbitMQ 等分布式队列,而非 Java 本地队列。

五、常见问题与解决方案

5.1 队列溢出问题

问题描述:有界队列(如 ArrayBlockingQueue)在生产者速度远大于消费者速度时,会出现队列满导致offer()返回 false 或put()阻塞的问题。

解决方案

  1. 增加消费者线程数量,提高消费速度;
  1. 扩大队列容量(需注意内存占用);
  1. 实现降级策略(如丢弃非核心任务);
  1. 使用无界队列(如 ConcurrentLinkedQueue),但需注意内存溢出风险。

5.2 线程安全问题

问题描述:使用非并发队列(如 PriorityQueue、LinkedList)在多线程环境下进行读写操作,会出现元素丢失、队列结构破坏等问题。

解决方案

  1. 改用线程安全的并发队列(如 ConcurrentLinkedQueue、ArrayBlockingQueue);
  1. 手动加锁(如使用synchronized或ReentrantLock),但会降低性能;
  1. 使用Collections.synchronizedQueue()包装非并发队列,本质是加锁实现线程安全。
相关推荐
普通网友18 小时前
IZT#73193
java·php·程序优化
rechol18 小时前
C++ 继承笔记
java·c++·笔记
Han.miracle21 小时前
数据结构——二叉树的从前序与中序遍历序列构造二叉树
java·数据结构·学习·算法·leetcode
Le1Yu1 天前
分布式事务以及Seata(XA、AT模式)
java
寒山李白1 天前
关于Java项目构建/配置工具方式(Gradle-Groovy、Gradle-Kotlin、Maven)的区别于选择
java·kotlin·gradle·maven
无妄无望1 天前
docker学习(4)容器的生命周期与资源控制
java·学习·docker
MC丶科1 天前
【SpringBoot 快速上手实战系列】5 分钟用 Spring Boot 搭建一个用户管理系统(含前后端分离)!新手也能一次跑通!
java·vue.js·spring boot·后端
千码君20161 天前
React Native:从react的解构看编程众多语言中的解构
java·javascript·python·react native·react.js·解包·解构
夜白宋1 天前
【word多文档docx合并】
java·word
@yanyu6661 天前
idea中配置tomcat
java·mysql·tomcat