在构建高并发系统时,队列是不可或缺的核心组件,用于解耦生产与消费、平滑流量、实现异步处理。Java集合框架提供了丰富而强大的队列实现,从基础的LinkedList到支持无锁并发的ConcurrentLinkedQueue,再到各种阻塞队列,每种都有其独特的内部结构与适用场景。理解它们的实现原理是进行精准技术选型、优化系统性能的关键。
Java队列体系概览
Java中的队列主要分为几个大类:
- 普通队列:如
LinkedList(作为队列使用时)、ArrayDeque(双端队列)、PriorityQueue(优先队列)。
- 并发非阻塞队列:如
ConcurrentLinkedQueue,基于无锁(CAS)算法实现。
- 阻塞队列:实现了
BlockingQueue接口,包括ArrayBlockingQueue(有界数组阻塞队列)、LinkedBlockingQueue(可选有界链表阻塞队列)、PriorityBlockingQueue(优先级阻塞队列)和SynchronousQueue(配对队列)。
为了便于选型,我们可以从支持操作、线程安全、阻塞特性、排序和容量上限几个维度进行对比:
| 队列类型 |
支持的操作 |
线程安全 |
阻塞操作 |
排序 |
上限 |
适用场景 |
| LinkedList (作为队列) |
FIFO |
否 |
否 |
否 |
无 |
单线程环境,频繁插入/删除操作 |
| ArrayDeque |
双端队列 |
否 |
否 |
否 |
无 |
需要快速插入和删除的双端队列操作 |
| PriorityQueue |
优先级队列 |
否 |
否 |
是 |
无 |
需要根据优先级处理元素的场景 |
| ConcurrentLinkedQueue |
FIFO |
是 |
否 |
否 |
无 |
多线程环境,高并发的无界队列操作 |
| ArrayBlockingQueue |
FIFO |
是 |
是 |
否 |
有 |
多线程环境,需要阻塞操作的有界队列 |
| LinkedBlockingQueue |
FIFO |
是 |
是 |
否 |
可选 |
多线程环境,需要阻塞操作的无界队列 |
| PriorityBlockingQueue |
优先级队列 |
是 |
是 |
是 |
无 |
多线程环境,需要优先级排序的阻塞队列 |
| SynchronousQueue |
非阻塞FIFO |
是 |
是 |
否 |
无 |
任务窃取、线程间单个元素传递 |
核心队列数据结构与原理分析
1. LinkedListQueue:基于链表的队列
LinkedList作为队列使用时,是一个基于双向链表实现的先进先出(FIFO)队列。
数据结构:
它由一系列Node(节点)对象组成。每个节点包含三部分:存储数据的data字段,指向前一个节点的prev引用,以及指向后一个节点的next引用。队列本身维护两个指针:head(头节点)指向队列的第一个元素,用于出队操作;tail(尾节点)指向队列的最后一个元素,用于入队操作。
执行流程:
- 开始:创建
LinkedListQueue实例。
- 入队操作:创建一个新节点,将其链接到当前
tail节点的next引用,然后更新tail指针指向新节点。
- 结束入队。
- 出队操作:通过
head指针获取当前头节点,将head指针更新为该节点的next引用,并返回原头节点的数据。
- 结束出队。
- 遍历队列:从
head节点开始,通过每个节点的next引用依次访问,直到next为null,读取每个节点的data。
- 结束遍历。
- 结束。
设计思考:它解决了数组固定长度和Vector同步性能差的问题,结合了链表高效增删的特性,提供了动态大小的FIFO管理,非常适合单线程下频繁插入删除的场景。
2. ArrayDeque:基于动态数组的双端队列
ArrayDeque是一个基于循环数组实现的高效双端队列,支持在头部和尾部进行O(1)时间复杂度的插入和删除。
数据结构:
ArrayDeque内部使用一个动态数组Elements来存储数据。它维护两个关键的索引:Head Index(头索引)指向队列的第一个元素,Tail Index(尾索引)指向下一个可插入元素的位置。数组在逻辑上是“循环”的,即当头或尾索引到达数组末尾时,会回绕到数组开头。
执行流程:
- 开始:创建
ArrayDeque实例。
- 入队操作(例如
offerLast):计算尾索引 -> 在数组的Tail Index位置添加元素到尾 -> 更新尾索引(通常为(tail + 1) & (array.length - 1))。
- 结束入队。
- 出队操作(例如
pollFirst):计算头索引 -> 移除并返回Head Index位置的元素 -> 更新头索引。
- 结束出队。
- 查看操作:
头部查看(peekFirst)直接读取Head Index处元素;尾部查看(peekLast)读取Tail Index前一个位置的元素。
- 结束查看。
- 结束。
设计思考:它融合了数组快速随机访问和链表高效增删的优点。通过循环数组,避免了在头部插入时移动大量元素。但其非线程安全,且扩容时(创建新数组并拷贝)会有性能损耗。
3. PriorityQueue:基于堆的优先队列
PriorityQueue是一个基于二叉堆(通常用数组实现)的队列,元素按照自然顺序或指定的Comparator进行优先级排序。
数据结构:
PriorityQueue的核心是一个Heap(堆)结构,底层由Array(数组)实现。堆是一种特殊的完全二叉树,其中每个节点的值都大于等于(最大堆)或小于等于(最小堆)其子节点的值。PriorityQueue默认是最小堆。可选的Comparator用于定义自定义的优先级比较规则。
执行流程:
- 开始:创建
PriorityQueue实例,可传入Comparator。
- 插入元素(
offer):将新元素添加到数组末尾,然后执行调整堆(上浮siftUp操作),使其满足堆的性质。
- 结束插入。
- 删除元素(
poll):移除顶部元素(数组第一个元素),将数组最后一个元素移至顶部,然后执行重新调整堆(下沉siftDown操作)。
- 结束删除。
- 查找顶部元素(
peek):直接读取数组第一个元素(堆顶),不修改堆。
- 结束查找。
- 结束。
设计思考:它解决了标准队列无法按优先级处理任务的问题。堆数据结构保证了每次获取(poll)的都是当前优先级最高的元素,插入和删除的复杂度为O(log n),适用于任务调度等场景。但它同样是非线程安全的。
4. ConcurrentLinkedQueue:无锁并发队列
ConcurrentLinkedQueue是一个线程安全的、无界的、非阻塞的先进先出队列,采用基于链接节点的无锁(Lock-Free)算法实现,是高并发场景下的高性能选择。
数据结构:
队列内部由多个Node节点通过next引用串联成单向链表。它维护两个volatile引用:head(头指针)指向队列的第一个节点(但不一定是有效的第一个元素,为了性能做了优化),tail(尾指针)指向队列的最后一个节点或最后一个节点的下一个节点。每个节点包含存储数据的item和指向下一个节点的next。
执行流程:
- 开始:创建
ConcurrentLinkedQueue实例。
- 入队操作(
offer):构造新节点 -> 使用CAS操作查找尾节点并尝试将新节点链接到其后(尾节点入队)-> 成功后可能更新尾指针(延迟更新策略)。
- 结束入队。
- 出队操作(
poll):查找头节点 -> 判断队列是否为空 -> 若非空,使用CAS操作移除头节点并更新头指针 -> 返回被移除元素。
- 结束出队。
- 查看队首元素(
peek):查找头节点 -> 返回头节点元素(若存在)。
- 结束查看。
- 结束。
设计思考:针对高并发环境下传统锁机制性能瓶颈的问题,它利用CAS原子指令实现了无锁并发控制,极大提升了多线程竞争下的吞吐量。其“无界”特性需要注意可能引发的内存问题。
5. ArrayBlockingQueue:有界数组阻塞队列
ArrayBlockingQueue是一个线程安全的、基于固定大小数组的先进先出阻塞队列,是经典生产者-消费者模型的实现。
数据结构:
它内部使用一个固定长度的Array(数组)存储元素,通过Head Pointer(头指针)和Tail Pointer(尾指针)来循环使用数组空间。队列的容量在创建时指定。线程安全通过一个ReentrantLock(可重入锁)和两个Condition(条件变量)notEmpty(非空)和notFull(非满)来保证。
执行流程与锁机制:
- 开始:创建指定容量的
ArrayBlockingQueue实例。
- 生产者线程尝试入队(
put/offer):
获取锁。
检查队列是否已满(count == items.length)。
- 若
已满,则生产者线程在notFull条件上等待。
- 若
未满(队列有空间),则执行元素入队(放入tail位置),更新尾指针,并唤醒在notEmpty上等待的消费者线程。
释放锁。
- 消费者线程尝试出队(
take/poll):
获取锁。
检查队列是否为空(count == 0)。
- 若
为空,则消费者线程在notEmpty条件上等待。
- 若
不为空(队列有元素),则执行元素出队(取出head位置元素),更新头指针,返回出队元素,并唤醒在notFull上等待的生产者线程。
释放锁。
设计思考:它提供了确定性的容量边界,防止资源耗尽。阻塞机制让生产者和消费者线程可以高效协作——队列满时生产者自动阻塞,队列空时消费者自动阻塞,简化了并发编程模型。锁的使用保证了强一致性,但在极高并发下可能成为性能热点。
6. LinkedBlockingQueue:可选有界链表阻塞队列
LinkedBlockingQueue是一个线程安全的、基于链表的阻塞队列,可以在构造时指定容量(有界),若不指定则默认为Integer.MAX_VALUE(近似无界)。
数据结构:
内部通过一个单向Linked List(链表)存储元素。每个Node节点包含数据Element Data和指向下一个节点的next引用。队列维护Head Pointer(头指针)和Tail Pointer(尾指针)。与ArrayBlockingQueue使用同一把锁不同,它采用“两把锁”策略:putLock(入队锁)和takeLock(出队锁),并配有相应的条件变量notFull和notEmpty。
执行流程与锁机制:
- 开始:创建
LinkedBlockingQueue实例(可选容量)。
- 入队操作(
put/offer):
- 获取
putLock。
- 检查队列是否已满(如果是有界队列)。
- 若未满,
将元素添加到队尾(创建新节点链接到tail.next),更新尾指针,增加计数。
- 释放
putLock。
- 如果本次入队前队列为空,则还需要获取
takeLock,然后通知等待的消费者(notEmpty.signal()),再释放takeLock。
- 出队操作(
take/poll):
- 获取
takeLock。
- 检查队列是否为空。
- 若不为空,
将元素从头部移除(移动head指针,注意head节点是哑元),更新头指针,减少计数,返回被移除的元素。
- 释放
takeLock。
- 如果本次出队前队列是满的,则还需要获取
putLock,然后通知等待的生产者(notFull.signal()),再释放putLock。
- 查看队首元素(
peek):需要获取takeLock,读取头部元素后释放锁。
设计思考:“两把锁”的设计使得入队和出队操作在大部分情况下可以完全并行,极大提升了吞吐量,特别适合生产者和消费者并发度都高的场景。其链表结构也带来了动态扩容的灵活性。
7. PriorityBlockingQueue:优先级阻塞队列
PriorityBlockingQueue是PriorityQueue的线程安全、阻塞版本。它结合了优先级排序和阻塞队列的特性。
数据结构:
底层同样是一个基于Array的Heap(堆),用于维护元素优先级。为了实现线程安全和阻塞,它内部使用了一个ReentrantLock,以及两个条件变量Not Empty Condition(对应notEmpty)和Not Full Condition(对应notFull,但由于其无界,notFull的条件通常不会触发,仅在显式扩容时可能涉及)。Head Index和Tail Index的概念隐含在堆的数组结构中。
设计思考:它在PriorityQueue的基础上,通过加锁实现了线程安全,并增加了阻塞的put和take方法。这使得它能够在多线程环境下,处理需要按优先级顺序执行的任务队列,例如实时系统中的任务调度。
8. SynchronousQueue:无缓冲区的配对队列
SynchronousQueue是一个不存储元素的特殊阻塞队列。每一个插入操作(put)必须等待一个对应的移除操作(take),反之亦然。它实现了生产者和消费者之间的直接“握手”传递。
数据结构:
它没有一个实质的元素容器(Virtual Container)。其核心是一个高效的、用于匹配入队线程(Offer Thread)和出队线程(Poll Thread)的协调器。内部通过栈或队列(公平模式)来管理等待的线程。Lock用于保护对内部等待队列的访问。
执行流程:
- 生产者线程到达,执行
put操作。
- 消费者线程到达,执行
take操作。
- 核心匹配逻辑:
- 如果当前有配对的、正在等待的相反操作线程(例如,生产者到达时已有消费者在等待),则直接完成元素传递,并唤醒等待的线程。
- 如果没有配对的线程,则当前线程会进入
等待队列(Thread Waits),直到配对的线程到来将其唤醒(Thread Signals)。
- 传递完成后,双方线程继续执行。
设计思考:它解决了传统有缓冲队列可能带来的延迟和内存占用问题,实现了极低延迟的线程间直接通信。适用于任务分发、线程池工作队列(如Executors.newCachedThreadPool)等需要高效直接传递的场景。吞吐量非常高,因为它省去了数据在队列中入队和出队的步骤。
// SynchronousQueue 应用示例:直接传递任务
import java.util.concurrent.SynchronousQueue;
public class SynchronousQueueExample {
public static void main(String[] args) {
SynchronousQueue<Integer> queue = new SynchronousQueue<>();
// 生产者线程
Thread producer = new Thread(() -> {
try {
Thread.sleep(1000); // 模拟生产耗时
int item = 1;
System.out.println("Producer offering item: " + item);
queue.put(item); // 将产品放入队列,会阻塞直到有消费者来取
System.out.println("Producer offered item: " + item);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 消费者线程
Thread consumer = new Thread(() -> {
try {
Integer item = queue.take(); // 从队列中取出产品,会阻塞直到有生产者放入
System.out.println("Consumer has taken item: " + item);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
总结与选型建议
通过对上述八种队列的深入剖析,我们可以得出以下选型思路:
- 单线程环境,需要双端操作:优先选择
ArrayDeque,性能最佳。
- 单线程环境,需要优先级处理:使用
PriorityQueue。
- 高并发,生产消费速度匹配,追求极高吞吐量:使用
ConcurrentLinkedQueue(无界)或SynchronousQueue(直接传递)。
- 经典的、资源可控的生产者-消费者模型:使用
ArrayBlockingQueue(固定边界)或LinkedBlockingQueue(弹性边界,吞吐量更高)。
- 多线程环境下的优先级任务调度:使用
PriorityBlockingQueue。
在实际的后端与架构设计中,队列的选择往往是系统性能和稳定性的关键。理解这些数据结构的底层原理,才能在高并发挑战下做出最合适的技术决策,构建出既健壮又高效的系统。
参考资料
[1] 高清图解28个高并发之数据结构/数据结构场景匹配技巧分析(高并发精通篇二), 微信公众号:mp.weixin.qq.com/s/bjwTPwbeFJAiME58wsZd0A
版权声明:本文由 云栈社区 整理发布,版权归原作者所有。