Queue 与 Deque 的区别
面试官您好,Queue
和 Deque
都是 Java 集合框架中定义队列行为的接口,但 Deque
是 Queue
的一个功能更强大的子接口。它们的主要区别在于:
一、Queue
(队列)
- 定义与特性:
Queue
接口位于java.util
包下,它继承自Collection
接口。- 它代表一种先进先出 (FIFO - First-In, First-Out) 的数据结构。元素从队列的一端(通常称为尾部或后端, rear/tail)加入,从另一端(通常称为头部或前端, front/head)移除。
- 它主要用于实现如任务队列、消息队列等需要按顺序处理元素的场景。
- 核心方法:
Queue
接口定义了两组核心方法,每组方法在队列满或空等特定条件下有不同的行为:- 抛出异常组:
add(E e)
: 向队尾添加元素。如果队列已满(对于有界队列),则抛出IllegalStateException
。remove()
: 移除并返回队头元素。如果队列为空,则抛出NoSuchElementException
。element()
: 返回队头元素(不移除)。如果队列为空,则抛出NoSuchElementException
。
- 返回特殊值组(通常更推荐用于并发编程或需要避免异常处理的场景):
offer(E e)
: 向队尾添加元素。如果队列已满(对于有界队列),则返回false
;否则返回true
。poll()
: 移除并返回队头元素。如果队列为空,则返回null
。peek()
: 返回队头元素(不移除)。如果队列为空,则返回null
。
- 抛出异常组:
- 常见实现类:
LinkedList
: 既实现了List
接口,也实现了Deque
接口(因此也实现了Queue
)。可以作为无界队列使用。ArrayBlockingQueue
: 一个基于数组的有界阻塞队列。PriorityQueue
: 一个基于优先堆的无界优先队列(元素按优先级出队,而非严格FIFO)。ConcurrentLinkedQueue
: 一个基于链接节点的无界线程安全队列。
二、Deque
(双端队列 - Double Ended Queue)
- 定义与特性:
Deque
接口位于java.util
包下,它继承自Queue
接口。- 它代表一种双端队列,即元素可以从队列的两端(头部和尾部)进行添加和移除。
- 因此,
Deque
不仅可以作为传统的 FIFO 队列使用,还可以作为 LIFO (Last-In, First-Out) 栈 (Stack) 使用。
- 核心方法:
Deque
接口继承了Queue
的所有方法,并在此基础上为双端操作增加了对应的方法。同样地,它也提供了抛出异常和返回特殊值两组方法:- 头部操作:
addFirst(E e)
/offerFirst(E e)
: 在队头添加元素。removeFirst()
/pollFirst()
: 移除并返回队头元素。getFirst()
/peekFirst()
: 返回队头元素(不移除)。
- 尾部操作:
addLast(E e)
/offerLast(E e)
: 在队尾添加元素 (这些方法等效于Queue
接口的add(E e)
/offer(E e)
)。removeLast()
/pollLast()
: 移除并返回队尾元素。getLast()
/peekLast()
: 返回队尾元素(不移除)。
- 作为栈使用时的方法:
push(E e)
: 等效于addFirst(E e)
(压栈)。pop()
: 等效于removeFirst()
(弹栈)。peek()
: 等效于peekFirst()
(查看栈顶元素,在Deque
中,peek()
默认操作头部,这与Queue
一致)。
- 头部操作:
- 常见实现类:
LinkedList
: 如前所述,它也实现了Deque
,是最常用的通用Deque
实现。ArrayDeque
: 一个基于可动态调整大小的数组实现的双端队列。通常比LinkedList
在作为队列或栈使用时性能更好(因为它避免了链表节点的额外开销和指针操作),推荐作为非并发场景下的栈或队列首选。ConcurrentLinkedDeque
: 一个基于链接节点的无界线程安全双端队列。LinkedBlockingDeque
: 一个基于链接节点的有界/无界阻塞双端队列。
三、主要区别总结:
特性 | Queue (队列) | Deque (双端队列) |
---|---|---|
继承关系 | java.util.Queue extends Collection | java.util.Deque extends Queue |
数据结构模型 | 先进先出 (FIFO) | 双端操作,可作为 FIFO 队列,也可作为 LIFO 栈 |
操作端点 | 只能在尾部添加,头部移除 | 可以在头部和尾部进行添加和移除 |
主要用途 | 任务队列、消息队列等单向顺序处理 | FIFO队列、LIFO栈、需要两端灵活操作的序列 |
方法集 | add/offer , remove/poll , element/peek | 继承 Queue 方法,并增加 First/Last 后缀的对应方法,以及 push/pop 等栈方法 |
灵活性 | 较低(单向) | 更高(双向) |
简单来说:
Queue
是一个基本的单向队列接口,定义了 FIFO 的行为。Deque
是Queue
的子接口,提供了更强大的双端操作能力,使其既能用作队列,也能方便地用作栈。- 在实际使用中,如果只需要标准的 FIFO 队列功能,使用
Queue
类型的引用并选择合适的实现类即可。如果需要双端操作或者想用作栈,那么应该使用Deque
类型的引用和其实现类(如ArrayDeque
或LinkedList
)。 - Java 官方现在更推荐使用
Deque
接口及其实现(如ArrayDeque
)来替代旧的java.util.Stack
类,因为Stack
类是基于Vector
实现的,性能较差且 API 设计有些陈旧。
ArrayDeque 与 LinkedList 的区别
面试官您好,ArrayDeque
和 LinkedList
都是 java.util.Deque
接口的优秀实现,它们都提供了双端队列的功能,可以作为队列或栈来使用。但它们在底层实现、特性支持和性能表现上有着明显的区别:
1. 底层数据结构:
ArrayDeque
:ArrayDeque
是基于一个可动态调整大小的循环数组 (resizable array / circular buffer)和两个指针(head
和tail
)来实现的。head
指向队列的头部元素,tail
指向队列尾部下一个可插入元素的位置。- 当元素从两端添加或移除时,主要是通过移动这些指针来实现。
LinkedList
:LinkedList
则是基于传统的双向链表来实现的。- 每个元素都封装在一个节点(
Node
)对象中,该节点包含元素本身以及指向前一个节点和后一个节点的引用。
2. 对null
元素的支持:
ArrayDeque
:不允许存储null
元素。如果尝试向ArrayDeque
中添加null
,会抛出NullPointerException
。这是其设计上的一个明确约定,以避免在poll()
或peek()
等操作返回null
时产生歧义(是队列为空还是获取到的元素就是null
)。LinkedList
:支持存储null
元素。你可以向LinkedList
中添加一个或多个null
。
3. 历史与引入版本:
ArrayDeque
:是在 JDK 1.6中才被引入的,相对较新。LinkedList
:早在 JDK 1.2时就已经存在,是集合框架的早期成员,并且它还实现了List
接口。
4. 内存占用与扩容机制:
ArrayDeque
:- 内存占用:通常比
LinkedList
更紧凑,因为它直接在数组中存储元素,避免了LinkedList
中每个节点对象的额外开销(如前后指针)。 - 扩容机制:当数组容量不足以容纳新元素时,
ArrayDeque
会进行扩容。它会创建一个新的、通常是当前容量两倍的数组,并将旧数组中的元素复制到新数组中。虽然单次扩容有一定成本,但由于扩容不是每次插入都发生,其均摊后的插入操作时间复杂度仍然是 O(1)。
- 内存占用:通常比
LinkedList
:- 内存占用:每个元素都需要一个额外的节点对象来包装,这会带来一定的内存开销。
- 扩容机制:
LinkedList
不需要传统意义上的“扩容”,因为它的大小是动态的,通过增删节点来调整。每次插入新元素时,都需要在堆上分配一个新的节点对象。删除元素时,节点对象会被垃圾回收。
5. 性能特性:
这是两者选择时非常重要的考量点:
- 访问操作 (随机访问
get(index)
):ArrayDeque
:不支持高效的随机访问。虽然底层是数组,但它作为Deque
的实现,并没有暴露按索引访问的方法。如果强行实现(不推荐),也需要考虑head
和tail
的循环特性。LinkedList
:作为List
实现时,其随机访问get(index)
的时间复杂度是 O(N)(最坏情况下需要遍历半个链表)。作为Deque
使用时,我们主要关心两端操作。
- 两端操作 (添加/删除:
addFirst/Last
,removeFirst/Last
,offerFirst/Last
,pollFirst/Last
):ArrayDeque
:这些操作的均摊时间复杂度是 O(1)。在不发生扩容的情况下,它们非常快,只需要移动指针和数组赋值。LinkedList
:这些操作的时间复杂度也是 O(1),因为只需要修改少数几个节点的指针。
- 迭代性能:
ArrayDeque
:由于数组的内存连续性,其迭代性能通常较好,缓存友好性更高。LinkedList
:迭代时需要进行指针跳转,缓存局部性较差,可能略慢。
- 插入/删除中间元素 (作为
List
时):ArrayDeque
:不直接支持。LinkedList
:O(1)(如果已经有指向该节点的引用),但查找该节点的开销是 O(N)。
- 总体性能考量 (作为
Deque
或Stack
使用时):- 正如您所说,从整体性能角度来看,当用作队列(FIFO)或栈(LIFO)时,
ArrayDeque
通常比LinkedList
更高效。 ArrayDeque
的优势在于其更低的内存开销(没有节点对象)和更好的缓存局部性。尽管它有扩容的成本,但均摊下来,其两端操作的常数因子通常更小。LinkedList
每次插入/删除都需要创建/销毁节点对象,这涉及到堆内存分配和垃圾回收的开销,在高频率操作时可能成为瓶颈。
- 正如您所说,从整体性能角度来看,当用作队列(FIFO)或栈(LIFO)时,
6. 适用场景总结:
ArrayDeque
:- 当需要一个高效的、非线程安全的队列或栈实现时,
ArrayDeque
通常是首选。 - 特别适合对性能要求较高,且不需要存储
null
元素的场景。
- 当需要一个高效的、非线程安全的队列或栈实现时,
LinkedList
:- 当需要一个同时具备
List
和Deque
功能的集合时(虽然不推荐频繁混用其List
和Deque
的操作,因为List
的某些操作在链表上效率不高)。 - 当需要存储
null
元素在队列或栈中时。 - 当对元素的插入和删除(尤其是在列表的中间位置,虽然作为
Deque
不常用)操作的“绝对 O(1)”保证(不考虑均摊)比内存分配开销更重要时(这种情况较少)。
- 当需要一个同时具备
简单来说:
如果你的主要需求是实现一个高效的队列或栈,并且不存储 null
,那么 ArrayDeque
往往是更好的选择。LinkedList
则在需要 List
功能或必须支持 null
时更有优势。
说一说 PriorityQueue
面试官您好,PriorityQueue
是 Java 集合框架中一个非常重要的队列实现,它在 JDK 1.5 被引入。与我们通常理解的先进先出(FIFO)队列不同,PriorityQueue
是一个基于优先级堆 (Priority Heap) 的无界优先队列。它的核心特性是:元素出队的顺序是根据元素的优先级来决定的,总是优先级最高的元素最先出队。
以下是我对 PriorityQueue
的一些关键理解:
- 底层数据结构与实现:
PriorityQueue
的底层是基于二叉堆 (Binary Heap)这种数据结构来实现的。具体来说,它通常使用一个可动态调整大小的数组来存储堆中的元素。- 这个数组按照二叉堆的层序遍历方式来组织元素,使得父节点和子节点之间的索引关系能够通过简单的数学运算(如
childIndex = parentIndex * 2 + 1
和parentIndex = (childIndex - 1) / 2
)来确定。
- 优先级确定与堆的类型:
- 元素的优先级是通过以下两种方式之一来确定的:
- 自然顺序:如果存储在
PriorityQueue
中的元素实现了java.lang.Comparable
接口,那么它们的优先级就由其compareTo()
方法定义的自然顺序决定。 - 自定义比较器:如果在创建
PriorityQueue
时提供了一个java.util.Comparator
对象,那么元素的优先级将由这个比较器定义的规则决定。
- 自然顺序:如果存储在
- 默认是小顶堆 (Min-Heap):如果不指定
Comparator
,并且元素具有自然顺序,PriorityQueue
默认实现的是一个小顶堆。这意味着具有较小(或“最低”)值的元素拥有较高的优先级,会被优先移除(即poll()
或peek()
方法返回的是当前队列中最小的元素)。 - 可配置为大顶堆 (Max-Heap):通过传入一个反向排序的
Comparator
(例如Collections.reverseOrder()
或者自定义的比较器),可以将PriorityQueue
配置为一个大顶堆,此时具有较大(或“最高”)值的元素拥有较高的优先级。
- 元素的优先级是通过以下两种方式之一来确定的:
- 核心操作与时间复杂度:
- 添加元素 (
add(E e)
或offer(E e)
):当添加一个新元素时,它通常被先放置在堆的末尾(数组的下一个可用位置),然后通过一个称为 “上浮” (siftUp / percolateUp) 的过程,与父节点比较并交换位置,直到它到达正确的位置以维持堆的性质。这个操作的时间复杂度是 O(log N),其中 N 是队列中元素的数量。 - 移除优先级最高的元素 (
poll()
或remove()
):移除操作总是移除堆顶元素(对于小顶堆是最小元素,大顶堆是最大元素)。移除后,通常会将堆的最后一个元素放到堆顶,然后通过一个称为 “下沉” (siftDown / percolateDown) 的过程,与子节点比较并交换位置,直到它到达正确的位置以恢复堆的性质。这个操作的时间复杂度也是 O(log N)。 - 查看优先级最高的元素 (
peek()
或element()
):这个操作只需要返回堆顶元素,不需要修改堆的结构,所以时间复杂度是 O(1)。
- 添加元素 (
- 特性与限制:
- 无界队列:
PriorityQueue
是逻辑上无界的,但其容量会根据需要动态增长(底层数组会扩容)。 - 非线程安全:
PriorityQueue
本身不是线程安全的。如果在多线程环境中使用,必须进行外部同步,或者使用java.util.concurrent.PriorityBlockingQueue
这个线程安全的替代品。 - 不支持
null
元素:尝试向PriorityQueue
中添加null
元素会抛出NullPointerException
。 - 不支持存储不可比较的对象 (non-comparable objects):如果元素没有实现
Comparable
接口,并且在创建PriorityQueue
时也没有提供Comparator
,那么在添加这类元素时会抛出ClassCastException
。 - 迭代顺序不保证:虽然
PriorityQueue
内部是有序的(堆有序),但通过迭代器(iterator()
)遍历PriorityQueue
时,元素的顺序是不保证的,并不一定是按优先级顺序排列的。如果需要按优先级顺序访问所有元素,应该重复调用poll()
方法。
- 无界队列:
- 应用场景:
- 正如您提到的,
PriorityQueue
在算法问题中非常有用:- 堆排序:虽然 Java 内置的排序通常不直接用
PriorityQueue
,但其思想是相关的。 - 求解 Top K 问题:例如,找到一个大集合中第 K 大(或小)的元素。可以使用一个大小为 K 的优先队列来维护当前的 Top K 元素。
- Dijkstra 算法和 Prim 算法:在图论中,用于寻找最短路径或最小生成树时,优先队列常用来高效地选取下一个要处理的顶点。
- 任务调度:根据任务的优先级来处理任务。
- 事件驱动模拟:管理待处理的事件,按事件发生时间排序。
- 堆排序:虽然 Java 内置的排序通常不直接用
- 正如您提到的,
总结来说,PriorityQueue
是一个非常有用的数据结构,它通过二叉堆实现了元素的优先级排序出队。它提供了 O(log N) 的插入和删除效率,以及 O(1) 的查看顶端元素效率。理解其默认的小顶堆行为、如何通过Comparator
自定义优先级、以及其非线程安全和不支持null
等特性,对于正确和高效地使用它至关重要。在算法设计和某些特定应用场景中,PriorityQueue
能够提供优雅且高效的解决方案。
什么是 BlockingQueue?
面试官您好,BlockingQueue
是 java.util.concurrent
包下的一个接口,它继承自 java.util.Queue
接口。其核心特性在于它是一个支持阻塞操作的队列。
具体来说,BlockingQueue
主要解决了在并发环境下,生产者线程和消费者线程之间数据交换时可能出现的两个核心问题:
- 当队列满时,生产者如何处理?
- 如果一个生产者线程尝试向一个已满的
BlockingQueue
中添加元素(通过put(E e)
方法),该生产者线程将会被阻塞 (block),直到队列中有空间可用(即某个消费者线程从队列中取走了元素)。
- 如果一个生产者线程尝试向一个已满的
- 当队列空时,消费者如何处理?
- 如果一个消费者线程尝试从一个空的
BlockingQueue
中获取元素(通过take()
方法),该消费者线程将会被阻塞 (block),直到队列中有新的元素被生产者放入。
- 如果一个消费者线程尝试从一个空的
BlockingQueue
的主要特点和方法:
- 线程安全:
BlockingQueue
的所有实现类都是线程安全的。多个线程可以并发地对同一个BlockingQueue
进行操作,而无需进行额外的外部同步。其内部实现通常依赖于锁(如ReentrantLock
)和条件变量(Condition
)来协调并发访问。 - 阻塞式插入和移除:这是
BlockingQueue
最核心的特性。put(E e)
: 将指定的元素插入此队列的尾部,如果队列已满,则等待空间可用。take()
: 获取并移除此队列的头部元素,如果队列为空,则等待元素可用。
- 可选的超时阻塞操作:除了无限期阻塞的
put
和take
,BlockingQueue
还提供了带有超时的版本:offer(E e, long timeout, TimeUnit unit)
: 将指定的元素插入此队列的尾部,如果在指定的等待时间内队列仍然已满,则返回false
;否则插入成功返回true
。poll(long timeout, TimeUnit unit)
: 获取并移除此队列的头部,如果在指定的等待时间内队列仍然为空,则返回null
;否则返回头部元素。
- 继承自
Queue
的非阻塞方法:BlockingQueue
也完整支持Queue
接口定义的非阻塞方法,这些方法在队列满或空时不会阻塞,而是立即返回或抛出异常:add(E e)
: 可能会抛出IllegalStateException
(如果队列有界且已满)。offer(E e)
: 如果队列已满,返回false
。remove()
: 可能会抛出NoSuchElementException
(如果队列为空)。poll()
: 如果队列为空,返回null
。element()
: 可能会抛出NoSuchElementException
(如果队列为空)。peek()
: 如果队列为空,返回null
。
- 有界与无界:
BlockingQueue
的实现可以是有界的 (bounded)或无界的 (unbounded)。- 有界队列:在创建时指定了最大容量。有界队列有助于防止资源耗尽,并能对生产者施加一定的流控。
- 无界队列:理论上可以存储无限数量的元素(受限于系统内存)。使用无界队列时需要注意,如果生产者速度远快于消费者,可能导致内存溢出。
常见的BlockingQueue
实现类:
ArrayBlockingQueue
: 一个基于数组实现的有界阻塞队列。元素按 FIFO(先进先出)顺序排序。内部使用单个锁和两个条件变量(notEmpty
,notFull
)来控制并发。LinkedBlockingQueue
: 一个基于链表实现的阻塞队列。可以是无界的(默认)或有界的(如果创建时指定了容量)。元素按 FIFO 顺序排序。内部通常使用两个锁(一个用于put
操作,一个用于take
操作,称为“two-lock queue”)和相应的条件变量,以提高并发性能。PriorityBlockingQueue
: 一个支持优先级排序的无界阻塞队列。元素出队顺序由其自然顺序或构造时提供的Comparator
决定。DelayQueue
: 一个无界阻塞队列,其中的元素只有在其延迟到期时才能被取出。队列的头部是延迟到期时间最早的元素。SynchronousQueue
: 一个不存储元素的阻塞队列。每个插入操作必须等待一个对应的移除操作,反之亦然。它非常适合传递性场景,即一手交钱一手交货。LinkedTransferQueue
: (JDK 7+) 一个基于链表的无界阻塞队列,实现了TransferQueue
接口,比SynchronousQueue
功能更强,支持transfer()
和tryTransfer()
这种更直接的“传递”语义。LinkedBlockingDeque
: 一个基于链表实现的双端阻塞队列。
主要应用场景:
- 生产者-消费者模式:这是
BlockingQueue
最经典和最主要的应用场景。生产者线程将任务或数据放入队列,消费者线程从队列中取出并处理。BlockingQueue
自动处理了线程间的同步和等待问题。 - 线程池的任务队列:Java 的
ThreadPoolExecutor
就使用BlockingQueue
来存储待执行的任务。 - 消息传递系统:在分布式或并发系统中,用于组件间的异步消息通信。
- 数据流处理:在数据管道中,用于缓存和传递数据块。
总结来说,BlockingQueue
是Queue
接口的一个重要扩展,它通过提供阻塞式的put
和take
操作(以及带超时的版本),极大地简化了并发环境下生产者和消费者之间的数据共享和同步问题。它是构建高效、健壮并发系统的关键组件之一。
ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别?
面试官您好,ArrayBlockingQueue
和 LinkedBlockingQueue
都是 java.util.concurrent.BlockingQueue
接口的常用实现,它们都是线程安全的,并且都支持阻塞式的生产者-消费者模式。但它们在设计和实现上有几个关键的区别:
1. 底层数据结构:
ArrayBlockingQueue
:它是基于定长数组实现的。内部维护一个固定大小的数组来存储队列元素。LinkedBlockingQueue
:它是基于链表实现的。内部通过节点(Node)互相链接来存储队列元素。
2. 有界性 (Boundedness):
ArrayBlockingQueue
:必须是有界的 (Bounded)。在创建ArrayBlockingQueue
时,必须指定其容量大小,并且这个容量一旦设定后不能改变。LinkedBlockingQueue
:可以是无界的,也可以是有界的。- 如果在创建时不指定容量,其默认容量是
Integer.MAX_VALUE
,实际上可以看作是无界的(受限于系统可用内存)。 - 如果在创建时指定了容量参数,那么它就成为一个有界的队列。
- 如果在创建时不指定容量,其默认容量是
3. 锁机制 (Concurrency Control):
这是两者在并发性能上的一个重要区别点:
ArrayBlockingQueue
:- 内部通常使用一个全局的
ReentrantLock
独占锁来控制对整个队列的并发访问。 - 并且,它使用这个锁关联的两个
Condition
对象(notEmpty
和notFull
)来管理生产者线程(等待队列不满)和消费者线程(等待队列不空)的阻塞和唤醒。 - 这意味着,在任何时刻,无论是生产者进行
put
操作还是消费者进行take
操作,它们都需要竞争同一个锁。这在生产者和消费者并发操作非常频繁时,可能会成为性能瓶颈。
- 内部通常使用一个全局的
LinkedBlockingQueue
:- 它采用了更细粒度的锁机制,即所谓的“两把锁”或“锁分离” (two-lock queue)。
- 内部维护两个独立的
ReentrantLock
:- 一个
putLock
(或类似的名称) 用于控制元素的入队(生产)操作。 - 一个
takeLock
(或类似的名称) 用于控制元素的出队(消费)操作。
- 一个
- 同时,每个锁也关联着各自的
Condition
对象(例如,putLock
关联notFull
条件,takeLock
关联notEmpty
条件)。 - 这种锁分离的设计允许生产者和消费者在大部分情况下可以并发地进行操作(例如,当队列既不空也不满时,一个生产者可以在队尾添加元素,同时一个消费者可以在队头移除元素,它们操作的是不同的锁和队列的不同部分)。这通常能带来比
ArrayBlockingQueue
更好的并发吞吐量,尤其是在生产者和消费者速度相对匹配的场景。
4. 内存占用与分配:
ArrayBlockingQueue
:- 由于底层是数组,它在创建时就需要预先分配固定大小的内存空间给这个数组,无论队列中实际存储了多少元素。
- 如果预设的容量远大于实际平均使用的元素数量,可能会造成一定的内存浪费。
LinkedBlockingQueue
:- 它采用的是动态内存分配。每个元素都封装在一个链表节点(
Node
)对象中,这个节点对象是在元素入队时才创建的。 - 因此,它的内存占用与队列中实际元素的数量成正比。
- 不过,每个节点对象本身会有一些额外的开销(用于存储前后指针等)。
- 它采用的是动态内存分配。每个元素都封装在一个链表节点(
5. 性能考量:
- 吞吐量:由于锁分离机制,
LinkedBlockingQueue
在高并发、生产者和消费者都比较活跃的情况下,通常能提供比ArrayBlockingQueue
更高的吞吐量。 - 可预测性/公平性:
ArrayBlockingQueue
在创建时可以指定一个可选的fair
参数来构造公平锁或非公平锁。公平锁可以保证等待时间最长的线程优先获得锁,但可能会降低整体吞吐量。LinkedBlockingQueue
的锁默认是非公平的。 - 垃圾回收:
LinkedBlockingQueue
由于频繁创建和销毁节点对象,可能会对垃圾回收产生更大的压力,尤其是在元素快速进出的场景。ArrayBlockingQueue
则相对稳定,因为它主要操作的是预分配的数组。
总结与选择建议:
特性 | ArrayBlockingQueue | LinkedBlockingQueue |
---|---|---|
底层结构 | 定长数组 | 链表 |
有界性 | 必须有界 | 可有界/可无界 (默认无界) |
锁机制 | 单一全局锁 (ReentrantLock ) | 两把锁 (putLock , takeLock ) - 锁分离 |
内存分配 | 创建时预分配 | 动态分配节点 |
并发吞吐量 | 通常较低 (因单一锁) | 通常较高 (因锁分离) |
null 支持 | 不允许 | 不允许 (作为 BlockingQueue 的通用约定) |
选择建议:
- 如果需要一个固定大小、有界的阻塞队列,并且对锁的公平性有要求,或者希望避免链表节点带来的额外GC压力,
ArrayBlockingQueue
是一个不错的选择。它的性能在某些特定负载下(例如,生产者或消费者一方远快于另一方)也可能表现良好。 - 如果需要一个容量可以非常大(甚至无界)的阻塞队列,或者追求更高的并发吞吐量(尤其是在生产者和消费者并发活跃时),
LinkedBlockingQueue
通常是更好的选择。 - 在大多数并发场景下,由于其锁分离带来的性能优势,
LinkedBlockingQueue
往往是更常用的阻塞队列实现。
当然,最佳选择还需结合具体的应用场景、性能测试和资源限制来综合判断。
参考JavaGuide