Java中双端队列的多种实现类详解
Java中双端队列的多种实现类详解
- 一、双端队列概述与核心接口
- 二、`ArrayDeque`:基于动态数组的双端队列实现
- 核心特性
- 源码分析:循环数组的奥秘
- 常用方法示例
- 适用场景
- 优缺点总结
- 三、`LinkedList`:基于双向链表的双端队列实现
- 核心特性
- 源码分析:节点结构与操作
- 常用方法示例
- 适用场景
- 优缺点总结
- 四、`ConcurrentLinkedDeque`:无锁线程安全双端队列
- 核心特性
- 源码分析:CAS操作实现原子性
- 多线程使用示例
- 适用场景
- 注意事项
- 五、`LinkedBlockingDeque`:有界阻塞双端队列
- 核心特性
- 源码分析:双锁实现并发控制
- 阻塞方法示例
- 适用场景
- 性能特点
- 六、性能对比与选择策略
- 插入操作性能测试(元素数量:100万)
- 选择策略总结
- 七、常见误区与注意事项
- 总结
双端队列(Deque,即Double Ended Queue)
是Java集合框架中的重要组成部分,它允许在队列的两端高效地插入和删除元素。本文我将深入剖析Java中四种主要的双端队列实现类:ArrayDeque
、LinkedList
、ConcurrentLinkedDeque
和LinkedBlockingDeque
,通过源码分析和性能对比,帮你掌握在不同应用场景下双端队列的最优选择。
一、双端队列概述与核心接口
双端队列(Deque)是一种特殊的队列,支持在队列的头部和尾部进行高效的插入和删除操作。在Java中,Deque
接口继承自Queue
接口,定义了一套标准的双端操作方法:
public interface Deque<E> extends Queue<E> {// 头部操作void addFirst(E e);boolean offerFirst(E e);E removeFirst();E pollFirst();E getFirst();E peekFirst();// 尾部操作void addLast(E e);boolean offerLast(E e);E removeLast();E pollLast();E getLast();E peekLast();// 栈操作void push(E e);E pop();// 迭代器Iterator<E> descendingIterator();
}
Deque接口提供了两套操作方法:一套在操作失败时抛出异常(如addFirst()
),另一套返回特殊值(如offerFirst()
返回false)。这种设计模式为不同使用场景提供了灵活性。
二、ArrayDeque
:基于动态数组的双端队列实现
核心特性
- 底层结构:循环数组,数组长度始终为2的幂
- 扩容机制:容量不足时自动扩容为原容量的2倍
- 性能特点:随机访问时间复杂度O(1),插入删除平均O(1)
- 限制条件:不允许存储
null
元素
源码分析:循环数组的奥秘
ArrayDeque
使用两个索引变量head
和tail
来标记队列的头部和尾部:
transient Object[] elements; // 存储元素的数组
transient int head; // 头部元素索引
transient int tail; // 尾部元素的下一个位置索引
插入元素时的索引计算采用位运算,确保在数组范围内循环:
// 添加到尾部
public void addLast(E e) {if (e == null)throw new NullPointerException();elements[tail] = e;// 位运算等价于 (tail + 1) % elements.lengthif ((tail = (tail + 1) & (elements.length - 1)) == head)doubleCapacity(); // 扩容
}
常用方法示例
Deque<String> deque = new ArrayDeque<>();// 添加元素
deque.addFirst("A"); // 头部插入
deque.offerLast("B"); // 尾部插入// 访问元素
String first = deque.peekFirst(); // 获取头部元素
String last = deque.getLast(); // 获取尾部元素// 删除元素
String removed = deque.pollFirst(); // 删除并返回头部元素
适用场景
- 需要高效的随机访问操作
- 作为栈或队列使用时,性能优于
LinkedList
- 不允许
null
元素的场景
优缺点总结
优点 | 缺点 |
---|---|
随机访问效率高 | 扩容操作开销较大 |
内存占用少 | 不适合频繁扩容的场景 |
无容量限制(自动扩容) | 初始容量选择需谨慎 |
三、LinkedList
:基于双向链表的双端队列实现
核心特性
- 底层结构:双向链表,每个节点包含前驱和后继引用
- 性能特点:插入删除时间复杂度O(1),随机访问O(n)
- 优势场景:频繁在队列中间插入删除元素的场景
- 特殊支持:允许存储
null
元素
源码分析:节点结构与操作
LinkedList
的节点定义:
private static class Node<E> {E item;Node<E> next;Node<E> prev;Node(Node<E> prev, E element, Node<E> next) {this.item = element;this.next = next;this.prev = prev;}
}
头部插入元素的实现:
public void addFirst(E e) {linkFirst(e);
}private void linkFirst(E e) {final Node<E> f = first;final Node<E> newNode = new Node<>(null, e, f);first = newNode;if (f == null)last = newNode;elsef.prev = newNode;size++;modCount++;
}
常用方法示例
Deque<Integer> deque = new LinkedList<>();// 添加元素
deque.push(10); // 等价于addFirst()
deque.offer(20); // 等价于addLast()// 遍历元素
for (Integer num : deque) {System.out.println(num);
}// 删除元素
Integer removed = deque.pop(); // 等价于removeFirst()
适用场景
- 需要频繁在队列中间插入或删除元素
- 元素数量不确定,需要动态调整
- 允许存储
null
元素的场景
优缺点总结
优点 | 缺点 |
---|---|
插入删除效率高 | 随机访问效率低 |
无需预分配空间 | 节点引用占用额外内存 |
适合动态扩容 | 遍历性能低于数组结构 |
四、ConcurrentLinkedDeque
:无锁线程安全双端队列
核心特性
- 线程安全机制:基于CAS(Compare-and-Swap)实现无锁算法
- 适用场景:高并发环境下的多线程操作
- 性能特点:读操作无阻塞,写操作通过CAS保证原子性
- 弱一致性迭代器:迭代时可能反映其他线程的修改
源码分析:CAS操作实现原子性
头部插入操作:
public void addFirst(E e) {linkFirst(e);
}private void linkFirst(E e) {checkNotNull(e);final Node<E> newNode = new Node<E>(e);retry:for (;;)for (Node<E> h = head, p = h, q;;) {if (p == null || (q = p.prev) == null) {// p是头节点或者列表为空newNode.lazySetNext(p);if (casHead(h, newNode)) {p.lazySetPrev(newNode);return;}// 重新读取headcontinue retry;}else if (p == q)// 节点已删除,重新开始continue retry;else// 继续向前查找p = q;}
}
多线程使用示例
import java.util.concurrent.ConcurrentLinkedDeque;public class ConcurrentDequeExample {private static final ConcurrentLinkedDeque<Integer> deque = new ConcurrentLinkedDeque<>();public static void main(String[] args) throws InterruptedException {// 生产者线程Thread producer = new Thread(() -> {for (int i = 0; i < 1000; i++) {deque.offerFirst(i);try {Thread.sleep(1);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}});// 消费者线程Thread consumer = new Thread(() -> {while (true) {Integer element = deque.pollLast();if (element != null) {System.out.println("Consumed: " + element);}}});producer.start();consumer.start();producer.join();}
}
适用场景
- 多线程环境下的无锁并发操作
- 读多写少的场景
- 需要高性能队列的分布式系统
注意事项
- 不支持
null
元素 - 迭代器弱一致性,不保证反映迭代期间的所有修改
- 批量操作(如
addAll
)不保证原子性
五、LinkedBlockingDeque
:有界阻塞双端队列
核心特性
- 阻塞机制:基于ReentrantLock实现,支持生产者-消费者模式
- 容量配置:可指定最大容量,默认
Integer.MAX_VALUE
- 应用场景:线程池工作队列、消息队列等
源码分析:双锁实现并发控制
/** Main lock guarding all access */
final ReentrantLock lock = new ReentrantLock();/** Condition for waiting takes */
private final Condition notEmpty = lock.newCondition();/** Condition for waiting puts */
private final Condition notFull = lock.newCondition();public void putFirst(E e) throws InterruptedException {if (e == null) throw new NullPointerException();Node<E> node = new Node<E>(e);final ReentrantLock lock = this.lock;lock.lock();try {while (!linkFirst(node))notFull.await();} finally {lock.unlock();}
}
阻塞方法示例
import java.util.concurrent.LinkedBlockingDeque;public class BlockingDequeExample {public static void main(String[] args) throws InterruptedException {LinkedBlockingDeque<Integer> deque = new LinkedBlockingDeque<>(2);// 生产者线程new Thread(() -> {try {deque.putFirst(1); // 队列满时阻塞System.out.println("Produced 1");deque.putLast(2);System.out.println("Produced 2");deque.putFirst(3);System.out.println("Produced 3");} catch (InterruptedException e) {Thread.currentThread().interrupt();}}).start();// 消费者线程new Thread(() -> {try {Thread.sleep(2000);Integer element = deque.takeLast(); // 队列空时阻塞System.out.println("Consumed: " + element);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}).start();}
}
适用场景
- 实现生产者-消费者模式
- 资源池(如数据库连接池)的实现
- 需要容量限制的并发场景
性能特点
- 读写操作均需获取锁,吞吐量低于
ConcurrentLinkedDeque
- 适合对数据一致性要求较高的场景
- 可配置容量防止内存溢出
六、性能对比与选择策略
插入操作性能测试(元素数量:100万)
操作类型 | ArrayDeque | LinkedList | ConcurrentLinkedDeque | LinkedBlockingDeque |
---|---|---|---|---|
头部插入 | 23ms | 35ms | 78ms | 122ms |
尾部插入 | 21ms | 38ms | 81ms | 119ms |
中间插入 | 184ms | 42ms | 105ms | 148ms |
选择策略总结
场景描述 | 推荐实现类 |
---|---|
单线程环境,频繁随机访问 | ArrayDeque |
单线程环境,频繁中间插入删除 | LinkedList |
多线程环境,无界队列 | ConcurrentLinkedDeque |
多线程环境,有界队列 | LinkedBlockingDeque |
作为栈或队列使用 | ArrayDeque |
七、常见误区与注意事项
-
内存占用问题
LinkedList
每个节点包含前驱和后继引用,内存占用高于ArrayDeque
ArrayDeque
扩容时需要复制数组,可能导致内存抖动
-
性能陷阱
- 在
LinkedList
中使用随机访问(如get(index)
)会导致O(n)时间复杂度 ConcurrentLinkedDeque
的批量操作(如addAll
)不是原子操作
- 在
-
线程安全误区
ArrayDeque
和LinkedList
是非线程安全的,多线程环境下必须外部同步ConcurrentLinkedDeque
不支持阻塞操作,不能替代LinkedBlockingDeque
总结
四种双端队列实现类各自适用场景:
- ArrayDeque:高性能动态数组实现,适合作为栈或队列使用
- LinkedList:灵活的链表结构,适合频繁插入删除操作
- ConcurrentLinkedDeque:无锁线程安全队列,适合高并发场景
- LinkedBlockingDeque:支持阻塞操作的有界队列,适合生产者-消费者模式
若这篇内容帮到你,动动手指支持下!关注不迷路,干货持续输出!
ヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノ