在队列的两端进行插入和删除
操作,在队列的 前端Front
和 后端Rear
高效地进行插入和删除操作。它融合了 普通队列(先进先出 - FIFO)
和 栈(后进先出 - LIFO)
的特性
public interface Deque<E> extends Queue<E> {}
比起基本队列Deque
为每种操作(插入、移除、检查)在队列的两端(头部和尾部)都提供了两组方法。一组在操作失败时抛出异常,另一组则返回特殊值(通常是 null
或 false
)。
操作 | 头部 (Front/First) | 尾部 (Rear/Last) |
---|---|---|
插入 | addFirst(e) / offerFirst(e) |
addLast(e) / offerLast(e) |
失败抛异常 / 失败返回 false | 失败抛异常 / 失败返回 false | |
移除 | removeFirst() / pollFirst() |
removeLast() / pollLast() |
空队列抛异常 / 空队列返回 null | 空队列抛异常 / 空队列返回 null | |
检查 | getFirst() / peekFirst() |
getLast() / peekLast() |
查看不删除 | 空队列抛异常 / 空队列返回 null | 空队列抛异常 / 空队列返回 null |
栈操作 | push(e) (等价于 addFirst(e) ) |
|
pop() (等价于 removeFirst() ) |
||
peek() (等价于 peekFirst() ) |
||
队列操作 | remove() (等价于 removeFirst() ) |
add(e) (等价于 addLast(e) ) |
(继承自Queue) | poll() (等价于 pollFirst() ) |
offer(e) (等价于 offerLast(e) ) |
element() (等价于 getFirst() ) |
||
peek() (等价于 peekFirst() ) |
||
其他 | removeFirstOccurrence(Object o) |
removeLastOccurrence(Object o) |
从前往后移除第一个匹配元素,成功返回true | 从后往前移除第一个匹配元素,成功返回true | |
contains(Object o) |
size() |
|
iterator() |
descendingIterator() (从尾到头遍历) |
1.双端操作
这是Deque
的核心。你可以在队列的头部
和尾部
执行插入、删除和检查操作
2.替代角色
使用push(e), pop(), peek()时,明确表示你在将Deque当作栈使
,Deque
是官方推荐替代遗留 Stack
类的方案3.容量限制
某些 Deque 实现(如 ArrayDeque)可能有固定容量限制。尝试在满队列中添加元素可能会导致异常或返回特殊值(取决于具体方法)
4.Null元素
Deque 的实现类可以允许或不允许null元素。ArrayDeque不允许null元素,而LinkedList允许
。使用 null 元素时要特别小心,因为某些方法(如 removeFirstOccurrence(null))可能会产生歧义
5.非线程安全
Deque的标准实现类(ArrayDeque, LinkedLis
t)不是线程安全的。线程安全方式
使用外部同步机制(如 Collections.synchronizedDeque())或使用并发包下的实现(如 LinkedBlockingDeque
)
ArrayDeque实现了Deque接口,基于动态数组
的数据提供了双端队列
的功能
public class ArrayDeque<E> extends AbstractCollection<E>
implements Deque<E>, Cloneable, Serializable
1.无参构造默认容量 16
// 创建一个初始容量为16的空队列
Deque<String> names = new ArrayDeque<>();
2.创建一个具有指定初始容量的空双端队列
//预分配足够空间存放大约1000个读数
Deque<Integer> sensorReadings = new ArrayDeque<>(1000);
// 但注意的是 自动调整为2的幂
ArrayDeque<Integer> deque2 = new ArrayDeque<>(100); // 实际容量128
3.创建一个包含指定集合 c 中所有元素的双端队列
元素的顺序由集合的迭代器返回顺序决定
List<String> list = Arrays.asList("Apple", "Banana", "Orange");
// 创建包含["Apple", "Banana", "Orange"]的队列
Deque<String> fruitDeque = new ArrayDeque<>(list);
重要提示:
如果传入的集合 c 是 null,会抛出 NullPointerException。
如果传入的集合 c 中包含 null 元素,在添加过程中(调用 c.iterator().next() 时)
会抛出 NullPointerException,因为 ArrayDeque 不允许 null 元素
数据结构
// 关键字段
transient Object[] elements; // 存储元素的数组
transient int head; // 头部指针(指向当前首元素)
transient int tail; // 尾部指针(指向下一个插入位置)
动态循环数组示意图
动态数组:数组的大小可随着数据量进行扩容
循环数组:数组的任何一点都可以被看作是起点或者终点
添加节点流程图
假设初始容量为 4(实际最小容量为 8,但为简化演示使用 4)
初始状态
数组:[null, null, null, null]
head = 0, tail = 0(空队列)
索引: 0 1 2 3
[null, null, null, null]
↑head/tail
步骤 1: 尾部添加 A (addLast("A"))
在tail位置插入A,tail后移:tail = (0 + 1) & 3 = 1
索引: 0 1 2 3
["A", null, null, null]
↑head ↑tail
步骤 2: 尾部添加 B (addLast("B"))
在 tail 位置插入 B,tail 后移:tail = (1 + 1) & 3 = 2
索引: 0 1 2 3
["A", "B", null, null]
↑head ↑tail
步骤 3: 头部添加 C (addFirst("C"))
head 前移:head = (0 - 1) & 3 = 3(循环到末尾) 在 head 位置插入 C
索引: 0 1 2 3
["A", "B", null, "C"]
↑tail ↑head
步骤 4: 尾部添加 D (addLast("D"))
在 tail 位置插入 Dtail 后移:tail = (2 + 1) & 3 = 3
此时 head == tail,触发扩容!
索引: 0 1 2 3
["A", "B", "D", "C"]
↑head/tail? (实际 head=3, tail=3)
步骤 5: 扩容(关键步骤)
计算新容量:原容量 4 → 新容量 8(翻倍)
创建新数组:[null, null, null, null, null, null, null, null]
复制元素(按逻辑顺序重排)
1.从原head开始:复制索引3的 "C" → 新数组索引 0
2.继续循环:复制索引0的 "A" → 新数组索引 1
3.复制索引1的 "B" → 新数组索引 2
4.复制索引2的 "D" → 新数组索引 3
重置指针:
head = 0(新数组头部)
tail = 4(新数组尾部下一个空位)
新数组索引: 0 1 2 3 4 5 6 7
["C", "A", "B", "D", null, null, null, null]
↑head ↑tail
再新增节点
头部添加 E (addFirst("E"))
head 前移:head = (0 - 1) & 7 = 7(循环到末尾),在 head 位置插入 E
索引: 0 1 2 3 4 5 6 7
["C", "A", "B", "D", null, null, null, "E"]
↑tail ↑head
尾部添加 F (addLast("F"))
在 tail 位置插入 F,tail 后移:tail = (4 + 1) & 7 = 5
索引: 0 1 2 3 4 5 6 7
["C", "A", "B", "D", "F", null, null, "E"]
↑tail ↑head
直到两种会师 那就再次扩容 2倍扩容
删除节点流程图
头部移除 (removeFirst())
取出 head 位置元素 "E",置空 head 位置
head 后移:head = (7 + 1) & 7 = 0
索引: 0 1 2 3 4 5 6 7
["C", "A", "B", "D", "F", null, null, null]
↑head ↑tail
尾部移除 (removeLast())
tail 前移:tail = (5 - 1) & 7 = 4
取出 tail 位置元素 "F" 置空 tail 位置
索引: 0 1 2 3 4 5 6 7
["C", "A", "B", "D", null, null, null, null]
↑head ↑tai
操作 | 方法 | 时间复杂度 | 说明 |
---|---|---|---|
头部插入 | addFirst(E) , push(E) |
O(1) | 插入失败抛出异常 |
offerFirst(E) |
O(1) | 插入失败返回 false | |
头部移除 | removeFirst() , pop() |
O(1) | 空队列抛出异常 |
pollFirst() |
O(1) | 空队列返回 null | |
头部查看 | getFirst() , peek() |
O(1) | 空队列抛出异常 |
peekFirst() |
O(1) | 空队列返回 null | |
尾部插入 | addLast(E) , add(E) |
O(1) | 插入失败抛出异常 |
offerLast(E) , offer(E) |
O(1) | 插入失败返回 false | |
尾部移除 | removeLast() |
O(1) | 空队列抛出异常 |
pollLast() |
O(1) | 空队列返回 null | |
尾部查看 | getLast() |
O(1) | 空队列抛出异常 |
peekLast() |
O(1) | 空队列返回 null | |
大小检查 | size() |
O(1) | 通过 head 和 tail 计算 |
包含检查 | contains(Object) |
O(n) | 线性搜索 |
清空 | clear() |
O(n) | 遍历数组置 null |
addFirst()
, offerFirst()
, removeFirst()
, pollFirst()
, getFirst()
, peekFirst()
addLast()
, offerLast()
, removeLast()
, pollLast()
, getLast()
, peekLast()
push
(等同于 addFirst)和 pop
(等同于 removeFirst)方法。add
(等同于 addLast)和 remove
(等同于 removeFirst)或 offer
和 poll
方法。使用场景
场景 | 推荐操作 |
---|---|
FIFO 队列 | offerLast(e) , pollFirst() |
LIFO 栈 | push(e) (=addFirst(e) ) , pop() (=removeFirst() ) |
滑动窗口 | 头尾快速插入/删除(如 LeetCode 239) |
任务调度 | 高效管理待执行任务队列 |
历史记录 | 固定容量实现 LRU(需配合移除逻辑) |
注意事项
场景 | 原因 |
---|---|
需要存储 null |
设计禁止 null 元素 |
高频随机访问 | 按索引访问中间元素需遍历(改用 ArrayList ) |
多线程无锁环境 | 非线程安全(改用 ConcurrentLinkedDeque 或 LinkedBlockingDeque ) |
精确控制容量增长 | 扩容策略固定为翻倍(若需定制策略需自行实现) |
// 1. 作为栈使用(LIFO)
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // addFirst(1)
stack.push(2);
int top = stack.pop(); // removeFirst() → 2
// 2. 作为队列使用(FIFO)
Deque<String> queue = new ArrayDeque<>(1000); // 预分配容量
queue.offer("A"); // addLast("A")
queue.offer("B");
String first = queue.poll(); // removeFirst() → "A"
// 3. 双端操作
ArrayDeque<Character> deque = new ArrayDeque<>();
deque.addFirst('X'); // [X]
deque.addLast('Y'); // [X, Y]
deque.removeFirst(); // [Y]
deque.removeLast(); // []
LinkedList是整个集合框架中最灵活的类之一,因为它同时实现了List Qeque Deque 三个数据结构
LinkedList详情介绍链接
ConcurrentLinkedDeque是线程安全的无界双端队列
,采用无锁算法CAS
实现,支持在队列两端进行高效并发操作。它是ConcurrentLinkedQueue
的双端队列版本
public class ConcurrentLinkedDeque<E>
extends AbstractCollection<E>
implements Deque<E>, java.io.Serializable {}
与 基本队列的非阻塞线程安全ConcurrentLinkedQueue
对比
特性 | ConcurrentLinkedDeque | ConcurrentLinkedQueue |
---|---|---|
操作端 | 双端(头尾操作) | 单端(尾部入队,头部出队) |
特殊方法 | addFirst() , removeLast() |
无 |
数据结构 | 双向链表 | 单向链表 |
内存开销 | 更高(每个节点两个指针) | 较低 |
使用场景 | 更广泛 | 标准队列场景 |
1.线程安全:基于无锁算法(CAS)实现
2.双端操作:支持队列头部和尾部的插入/移除
3.无界队列:理论上可无限扩展(受内存限制)
4.非阻塞:操作不会导致线程阻塞
5.FIFO/LIFO 混合:可作为队列或栈使用
6.不允许 null 元素:插入 null 会抛出 NullPointerException
阻塞和非阻塞的主要区别在于是否等待对方响应
阻塞调用是指调用结果返回之前,当前线程会被挂起,一直处于等待消息通知,不能执行其他业务
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
ConcurrentLinkedDeque 是非阻塞线程的特性区别对比 阻塞线程的类
特性 | 阻塞队列 (如 ArrayBlockingQueue) | 非阻塞队列 (ConcurrentLinkedDeque) |
---|---|---|
线程挂起 | 竞争失败时挂起等待 | 失败时自旋重试,无挂起 |
开销 | 上下文切换开销大 | 无上下文切换,仅 CPU 自旋 |
吞吐量 | 高竞争下下降显著 | 高并发场景下更优 |
简单来说:阻塞的竞争失败挂起等待,非阻塞的不挂起不断重试直到成功,所以性能开销更大点
头部操作
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()
:查看但不移除尾部元素通用操作
boolean add(E e)
:尾部添加(等价于 addLast
)E remove()
:头部移除(等价于 removeFirst
)E element()
/ E peek()
:查看头部元素数据结构:双向链表节点
双向链表节点结构
private static final class Node<E> {
volatile Node<E> prev; // 前驱指针
volatile E item; // 存储元素
volatile Node<E> next; // 后继指针
// CAS 操作方法
boolean casItem(E cmp, E val) { /*...*/ }
void lazySetNext(Node<E> val) { /*...*/ }
boolean casNext(Node<E> cmp, Node<E> val) { /*...*/ }
// ... 类似的前驱指针操作方法
}
队列头尾指针分离
private transient volatile Node<E> head;
private transient volatile Node<E> tail;
实现原理
核心点
1.数据结构:双向链表节点(Node)
2.无锁算法:使用CAS操作更新指针
3.头尾指针:volatile修饰的头节点(head)和尾节点(tail)
4.协作式删除机制:逻辑删除(标记item为null)和物理删除(解除链接)
5.一致性:弱一致性迭代器和视图
6.惰性更新策略:头尾指针不总是精确指向端点
7.最终一致性:通过后续操作逐步修正位置
已元素删除算法(以 pollFirst 为例)
public E pollFirst() {
restartFromHead:
for (;;) {
// 步骤1:定位头节点
Node<E> h = head, p = h;
// 步骤2:查找首个有效节点
while (p.item == null) {
Node<E> next = p.next;
if (next == null) return null; // 队列为空
p = next;
}
// 步骤3:逻辑删除 (标记item为null)
if (!p.casItem(p.item, null)) continue;
// 步骤4:物理解除链接
Node<E> next = p.next;
if (next != null && next.prev == p) {
next.casPrev(p, p.prev); // 更新后继的前驱
}
Node<E> prev = p.prev;
if (prev != null && prev.next == p) {
prev.casNext(p, p.next); // 更新前驱的后继
}
// 步骤5:更新头指针
if (p != h) {
updateHead(); // 惰性更新head
}
return p.item;
}
}
协作式删除节点
在步骤3:逻辑删除 (标记item为null)
// 在指针中嵌入状态信息(实际通过节点状态实现)
if (node.item == null) {
// 节点已被删除,需要特殊处理
helpUnlink(node); // 协助解除链接
continue; // 重试操作
}
当线程A尝试删除节点X时:先将 X.item 置为 null(逻辑删除)
线程B若发现 X.item == null:主动调用 helpUnlink(x) 协助物理解除链接,更新相邻节点的指针
减少不必要的CAS操作
迭代器实现原理:弱一致性迭代器
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
private Node<E> nextNode;
private E nextItem;
Itr() {
advance(); // 初始化时获取首个有效节点
}
void advance() {
// 从当前节点开始查找下一个有效节点
do {
nextNode = (nextNode == null) ? first() : nextNode.next;
} while (nextNode != null && nextNode.item == null);
nextItem = (nextNode != null) ? nextNode.item : null;
}
}
特点:
快照隔离:迭代开始时获取当前头指针
跳过已删除节点:自动过滤 item == null 的节点
弱一致性:可能反映迭代开始后的部分修改
惰性更新策略
//前向指针修正:
void fixPrev(Node<E> p) {
for (;;) {
Node<E> q = p.prev;
Node<E> next = p.next;
if (q != null && q.next != p) {
q.casNext(q.next, p); // 修复前驱的后继
}
if (next != null && next.prev != p) {
next.casPrev(next.prev, p); // 修复后继的前驱
}
// 检查是否修复完成
if ((q == null || q.next == p) &&
(next == null || next.prev == p)) {
break;
}
}
}
修复后,最终一致性
设计亮点
1.无锁并发:基于 Michael-Scott 算法的双端扩展,使用 CAS 实现线程安全
2.惰性更新策略:不立即更新反向指针(允许暂时不一致), 后续操作会修复不一致状态
3.删除标记技术:协作删除:线程A的物理删除 和 后续线程B的物理删除
4.自我修复机制:线程遇到不一致状态时主动修复链表,保证最终一致性
性能总结
操作 | 时间复杂度 | 并发性能 |
---|---|---|
addFirst() |
O(1) | 极高 |
pollLast() |
O(1) | 高 |
size() |
O(n) | 低 |
contains() |
O(n) | 中 |
注意事项
1.弱一致性方法:
size()
需要遍历整个链表,结果可能过时contains()
可能返回瞬时状态2.内存消耗:
3.精确性要求:
// 不要依赖size()进行关键决策
if (deque.size() > threshold) {
// 这个判断可能不可靠!
}
适用场景
1.工作窃取算法:
// 工作者线程从自己队列头部获取任务
Runnable task = localDeque.pollFirst();
// 当本地队列为空时,从其他队列尾部窃取
if (task == null) {
task = otherDeque.pollLast();
}
2.多生产者-多消费者系统:
3.实时事件处理系统:
4.并发缓存实现:
最佳实践
1.首选特定端方法:
// 明确意图
deque.offerFirst(item); // 比 push() 更清晰
2.空队列处理:
// 使用poll而不是remove避免异常
E item = deque.pollFirst();
if (item != null) {
// 处理元素
}
3.批量处理优化:
// 批量提取多个元素
List<E> batch = new ArrayList<>(BATCH_SIZE);
while (batch.size() < BATCH_SIZE) {
E item = deque.pollFirst();
if (item == null) break;
batch.add(item);
}
和Deque其余子类 性能对比
操作 | ConcurrentLinkedDeque | LinkedBlockingDeque | ArrayDeque (非线程安全) |
---|---|---|---|
头部插入 (100线程) | 12 ms | 45 ms | N/A |
尾部插入 (100线程) | 13 ms | 48 ms | N/A |
头部删除 (100线程) | 11 ms | 42 ms | N/A |
内存开销 (100万元素) | ~48 MB | ~32 MB | ~24 MB |
ConcurrentLinkedDeque
是 Java 并发编程中的高级工具:
在需要高并发双端操作的场景下,ConcurrentLinkedDeque
提供了最优秀的并发性能,是构建高性能并发系统的理想选择
1.作为并发栈(LIFO)
ConcurrentLinkedDeque<String> stack = new ConcurrentLinkedDeque<>();
// 压栈
stack.push("Task1"); // addFirst
stack.push("Task2");
// 弹栈
String task = stack.pop(); // removeFirst
2.作为双端队列
ConcurrentLinkedDeque<Integer> deque = new ConcurrentLinkedDeque<>();
// 生产者A - 尾部添加
new Thread(() -> {
for (int i = 0; i < 100; i++) {
deque.addLast(i); // 尾部添加
}
}).start();
// 生产者B - 头部添加
new Thread(() -> {
for (int i = -1; i > -100; i--) {
deque.addFirst(i); // 头部添加
}
}).start();
// 消费者 - 头部处理
new Thread(() -> {
while (true) {
Integer num = deque.pollFirst(); // 头部取出
if (num != null) {
System.out.println("Processed: " + num);
}
}
}).start();
特性 | ArrayDeque | LinkedList |
---|---|---|
数据结构 | 动态循环数组 | 双向链表 |
内存占用 | 更紧凑(无节点开销) | 每个元素额外24字节 |
随机访问 | O(n) | O(n)(实际更慢) |
迭代性能 | 更高(缓存友好) | 较低 |
插入/删除(头尾) | O(1) 分摊时间 | O(1) |
插入/删除(中间) | O(n) | O(1)(已知位置) |
null 支持 | ❌ 禁止 | ✅ 允许 |
多线程安全 | ❌ 需要外部同步 | ❌ 需要外部同步 |
扩容成本 | 较高(复制数组) | 无 |
Deque
是一个功能强大且灵活的接口,统一了队列和栈的抽象。它提供了丰富的双端操作方法(包括异常抛出型和特殊值返回型)。ArrayDeque
通常是需要高效双端操作且不需要 List
功能或 null
元素时的最佳选择。LinkedList
在需要同时使用 Deque
和 List
功能或需要 null
元素时是唯一选择。理解不同方法的区别以及两种主要实现的特性,对于正确有效地使用 Deque
至关重要。