1. 概述
LinkedBlockingDeque 是 Java 并发包(java.util.concurrent)中提供的一个基于双向链表实现的可选有界阻塞双端队列 。它实现了 BlockingDeque 接口,继承了 BlockingQueue 接口的所有语义,并扩展了在队列两端进行插入、移除和获取操作的能力。所有的插入/移除操作在必要时都可以阻塞等待,直到空间可用或元素就绪。
核心特点
- 双向链表结构 :每个节点维护
prev和next两个指针,支持从队首和队尾高效操作。 - 可选有界性 :构造时可指定最大容量,不指定时默认为
Integer.MAX_VALUE,表现为无界(实际上受限于内存)。 - 双端阻塞操作 :提供
putFirst、takeLast等方法,允许将队列当作双端队列(Deque) 、栈(Stack) 或 队列(Queue) 使用。 - 单锁设计 :所有入队/出队操作共享一把
ReentrantLock,配合两个条件变量notEmpty和notFull实现线程同步。 - 动态内存分配:节点在插入时创建,移除时断开引用便于 GC,无容量扩容开销。
典型应用场景
- 工作窃取(Work Stealing)算法 :每个工作线程维护自己的双端队列,从头部取任务,空闲线程从其他队列尾部窃取任务(如
ForkJoinPool中的WorkQueue实际上使用了数组,但思想一致)。 - 双端生产者-消费者模型:生产者可在队尾追加,消费者可从队首消费(FIFO),另一消费者可从队尾消费(LIFO)。
- 用作阻塞栈 :使用
putFirst压栈,takeFirst出栈。 - 流量控制:利用有界容量限制资源使用,避免内存溢出。
与其他阻塞队列的简要对比
| 特性 | LinkedBlockingDeque | LinkedBlockingQueue | ArrayBlockingQueue | ConcurrentLinkedDeque |
|---|---|---|---|---|
| 数据结构 | 双向链表 | 单向链表 | 数组 | 双向链表(非阻塞) |
| 双端操作 | 支持 | 不支持 | 不支持 | 支持 |
| 有界性 | 可选有界(默认无界) | 可选有界(默认无界) | 固定有界 | 无界 |
| 锁机制 | 单锁(ReentrantLock) |
双锁(putLock 和 takeLock) |
单锁 | 无锁(CAS) |
| 阻塞特性 | 阻塞插入/移除/获取 | 阻塞插入/移除/获取 | 阻塞插入/移除/获取 | 非阻塞 |
| size() 复杂度 | O(1) | O(1) | O(1) | O(n) |
本文重点在于深入剖析 LinkedBlockingDeque 的源码实现,并在适当位置与 LinkedBlockingQueue 和 ArrayBlockingQueue 进行详细对比。
2. 核心方法说明
LinkedBlockingDeque 提供了丰富的双端操作 API。以下按功能分组,列出主要方法及其行为。所有方法均不允许插入 null 元素 ,否则抛出 NullPointerException。
2.1 双端插入方法
| 方法签名 | 参数 | 返回值 | 阻塞行为 | 异常情况 |
|---|---|---|---|---|
void putFirst(E e) |
e:要插入的元素 |
无 | 若队列已满,线程阻塞等待直到空间可用。响应中断 | InterruptedException 如果被中断;NullPointerException 如果 e == null |
void putLast(E e) |
同上 | 无 | 同 putFirst,从队尾插入 |
同上 |
boolean offerFirst(E e) |
同上 | true 成功;false 队列已满 |
不阻塞,满则立即返回 false |
NullPointerException 如果 e == null |
boolean offerLast(E e) |
同上 | 同上 | 同上 | 同上 |
boolean offerFirst(E e, long timeout, TimeUnit unit) |
e:元素;timeout:等待时间;unit:时间单位 |
true 成功;false 超时 |
队列满时等待指定时间,超时返回 false。响应中断 |
InterruptedException;NullPointerException |
boolean offerLast(E e, long timeout, TimeUnit unit) |
同上 | 同上 | 同上 | 同上 |
void addFirst(E e) |
e:元素 |
无 | 不阻塞,内部调用 offerFirst,满时抛出异常 |
IllegalStateException("Deque full");NullPointerException |
void addLast(E e) |
同上 | 无 | 同上 | 同上 |
2.2 双端移除方法
| 方法签名 | 参数 | 返回值 | 阻塞行为 | 异常情况 |
|---|---|---|---|---|
E takeFirst() |
无 | 移除的队首元素 | 若队列为空,线程阻塞等待直到有元素。响应中断 | InterruptedException |
E takeLast() |
无 | 移除的队尾元素 | 同上 | 同上 |
E pollFirst() |
无 | 队首元素;若队列空返回 null |
不阻塞,空则立即返回 null |
无 |
E pollLast() |
无 | 队尾元素;若队列空返回 null |
同上 | 无 |
E pollFirst(long timeout, TimeUnit unit) |
timeout,unit |
元素;超时返回 null |
队列空时等待指定时间,超时返回 null。响应中断 |
InterruptedException |
E pollLast(long timeout, TimeUnit unit) |
同上 | 同上 | 同上 | 同上 |
E removeFirst() |
无 | 队首元素 | 不阻塞,内部调用 pollFirst,空时抛出异常 |
NoSuchElementException |
E removeLast() |
无 | 队尾元素 | 同上 | NoSuchElementException |
2.3 双端获取方法(不移除)
| 方法签名 | 参数 | 返回值 | 阻塞行为 | 异常情况 |
|---|---|---|---|---|
E peekFirst() |
无 | 队首元素;空则 null |
非阻塞 | 无 |
E peekLast() |
无 | 队尾元素;空则 null |
非阻塞 | 无 |
E getFirst() |
无 | 队首元素 | 非阻塞,内部调用 peekFirst,空时抛出异常 |
NoSuchElementException |
E getLast() |
无 | 队尾元素 | 同上 | NoSuchElementException |
2.4 队列方法(继承自 BlockingQueue)
这些方法等价于特定的双端操作:
| 方法签名 | 等价于 | 备注 |
|---|---|---|
void put(E e) |
putLast(e) |
阻塞插入队尾 |
E take() |
takeFirst() |
阻塞移除队首 |
boolean offer(E e) |
offerLast(e) |
非阻塞插入队尾 |
E poll() |
pollFirst() |
非阻塞移除队首 |
E peek() |
peekFirst() |
获取队首但不移除 |
boolean add(E e) |
addLast(e) |
满时抛异常 |
E remove() |
removeFirst() |
空时抛异常 |
E element() |
getFirst() |
获取但不移除,空抛异常 |
2.5 其他重要方法
| 方法签名 | 参数 | 返回值 | 描述 | 备注 |
|---|---|---|---|---|
int size() |
无 | 当前元素个数 | 获取锁后读取 count 字段,O(1) 精确值 |
|
int remainingCapacity() |
无 | 剩余可用容量 | capacity - count,无界时返回 Integer.MAX_VALUE |
|
int drainTo(Collection<? super E> c) |
c:目标集合 |
转移的元素数量 | 将队首元素全部转移到 c,等价于循环 pollFirst() |
持有锁一次性转移,高效 |
int drainTo(Collection<? super E> c, int maxElements) |
同上,maxElements 最大转移数 |
转移的数量 | 最多转移 maxElements 个元素 |
|
boolean remove(Object o) |
o:要移除的元素 |
true 若移除成功 |
遍历链表查找并移除第一个 相等元素(equals 比较) |
O(n) 时间,持有锁 |
boolean contains(Object o) |
同上 | true 若存在 |
遍历链表查找 | O(n) 时间 |
Iterator<E> iterator() |
无 | 正向迭代器 | 弱一致性,不抛 ConcurrentModificationException |
|
Iterator<E> descendingIterator() |
无 | 反向迭代器 | 从队尾向队首遍历 |
3. 核心原理与源码分析(基于 JDK 8)
3.1 数据结构
LinkedBlockingDeque 内部维护一个双向链表 ,节点定义为静态内部类 Node<E>:
java
static final class Node<E> {
E item; // 元素值,移除时置为 null
Node<E> prev; // 前驱指针
Node<E> next; // 后继指针
Node(E x) { item = x; }
}
核心成员变量如下(摘自 JDK 8 源码):
java
public class LinkedBlockingDeque<E> extends AbstractQueue<E>
implements BlockingDeque<E>, java.io.Serializable {
// 链表首节点(不变量:first.item != null 当非空时)
transient Node<E> first;
// 链表尾节点(不变量:last.item != null 当非空时)
transient Node<E> last;
// 当前元素个数(锁保护)
private transient int count;
// 队列容量(final 不可变)
private final int capacity;
// 主锁,所有操作都需获取此锁
final ReentrantLock lock = new ReentrantLock();
// 队列非空条件(用于 take/poll 等)
private final Condition notEmpty = lock.newCondition();
// 队列未满条件(用于 put/offer 等)
private final Condition notFull = lock.newCondition();
}
关键设计说明:
count是普通的int,所有对其的读写都在lock保护下进行,因此size()能返回精确值且为 O(1)。capacity为final,一旦构造确定不可更改。- 使用 单锁 控制所有访问,这是与
LinkedBlockingQueue双锁设计的最大不同。原因在于双端操作需要同时修改first和last,分离锁可能导致复杂的协调甚至死锁,单锁简化了并发控制。 - 空队列时
first == last == null,有元素时first指向第一个节点(其prev == null),last指向最后一个节点(其next == null)。
3.2 构造器分析
共有三个构造器:
java
// 默认无界构造器
public LinkedBlockingDeque() {
this(Integer.MAX_VALUE);
}
// 指定容量构造器
public LinkedBlockingDeque(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
}
// 从集合初始化构造器
public LinkedBlockingDeque(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock lock = this.lock;
lock.lock(); // 加锁保证可见性和一致性
try {
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (!linkLast(e)) // 逐个链接,若满则抛异常
throw new IllegalStateException("Deque full");
}
} finally {
lock.unlock();
}
}
初始化完成后,first 和 last 均为 null,count 为 0。从集合初始化时,元素按迭代器顺序从队尾依次加入。
3.3 双端插入核心流程
以 putFirst(E e) 为例,源码及注释如下:
java
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 {
// 若队列满,则阻塞等待 notFull 条件
while (count == capacity)
notFull.await();
linkFirst(node); // 将新节点链接到队首
} finally {
lock.unlock();
}
}
linkFirst 链接逻辑:
java
private void linkFirst(Node<E> node) {
Node<E> f = first;
node.next = f; // 新节点的 next 指向旧 first
first = node; // 更新 first 指针
if (f == null)
last = node; // 之前为空,last 也指向该节点
else
f.prev = node; // 旧 first 的 prev 回指新节点
if (++count == 1)
notEmpty.signal(); // 第一个元素加入时唤醒等待的消费者
}
putLast 与 linkLast 对称,在此略去源码。
非阻塞版本 offerFirst:
java
public boolean offerFirst(E e) {
if (e == null) throw new NullPointerException();
Node<E> node = new Node<E>(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == capacity)
return false;
linkFirst(node);
return true;
} finally {
lock.unlock();
}
}
对比 LinkedBlockingQueue :LinkedBlockingQueue 使用分离锁(putLock 和 takeLock),允许并发的入队和出队操作,但在双端场景下分离锁无法保证两个方向的一致性,因此 LinkedBlockingDeque 回归单锁。
3.4 双端移除核心流程
takeFirst() 源码:
java
public E takeFirst() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E x;
while ( (x = unlinkFirst()) == null)
notEmpty.await(); // 队列空,等待
return x;
} finally {
lock.unlock();
}
}
unlinkFirst 解链逻辑:
java
private E unlinkFirst() {
Node<E> f = first;
if (f == null)
return null; // 空队列
Node<E> n = f.next;
E item = f.item;
f.item = null;
f.next = f; // help GC(自引用)
first = n;
if (n == null)
last = null; // 移除后为空
else
n.prev = null;
--count;
if (count == capacity - 1)
notFull.signal(); // 从满变为未满,唤醒生产者
return item;
}
注意:
f.next = f是一个巧妙的设计,帮助 GC 回收节点(因为此时仍有外部引用?实际上是为了让节点自身不可达,但效果有限,更多是惯用法)。- 移除成功后若队列从满变为未满,唤醒一个在
notFull上等待的生产者。
takeLast 与 unlinkLast 对称。
3.5 通用移除操作 remove(Object o)
remove 方法调用内部 unlink(Node<E> x),需要先遍历链表找到目标节点:
java
public boolean remove(Object o) {
if (o == null) return false;
final ReentrantLock lock = this.lock;
lock.lock();
try {
for (Node<E> p = first; p != null; p = p.next) {
if (o.equals(p.item)) {
unlink(p);
return true;
}
}
return false;
} finally {
lock.unlock();
}
}
void unlink(Node<E> x) {
Node<E> p = x.prev;
Node<E> n = x.next;
if (p == null) {
unlinkFirst(); // 是首节点,重用解链逻辑
} else if (n == null) {
unlinkLast(); // 是尾节点
} else {
p.next = n;
n.prev = p;
x.item = null;
// 不改变 first/last
--count;
if (count == capacity - 1)
notFull.signal();
}
}
3.6 单锁设计的深入分析
| 设计点 | LinkedBlockingDeque | LinkedBlockingQueue | ArrayBlockingQueue |
|---|---|---|---|
| 锁数量 | 1 个 ReentrantLock |
2 个(putLock 和 takeLock) |
1 个 ReentrantLock |
| 锁粒度 | 粗粒度,所有操作互斥 | 入队与出队可并发 | 粗粒度,所有操作互斥 |
| 双端支持 | 必须,单锁简化协调 | 不支持双端,分离锁提升吞吐量 | 不支持双端,数组结构天然需要单锁 |
为什么 LinkedBlockingDeque 不采用双锁?
- 双端操作(如同时
putFirst和putLast)都需要修改first和last,如果分离为"首锁"和"尾锁",当队列只有一个元素时,两个操作会竞争不同锁,极易造成死锁或复杂的状态判断。单锁虽然降低了并发度,但保证了正确性和简洁性。 - 在双端操作场景(如工作窃取)中,对锁的竞争原本就较高,单锁已足够。
性能影响 :单锁意味着即使一个线程在队首操作,另一个线程也无法同时操作队尾。与 LinkedBlockingQueue 相比,纯 FIFO 场景下吞吐量较低。但在双端场景中,这是唯一阻塞实现,且其性能在多数实际应用中可接受。
3.7 容量可选的实现
容量在构造时固定。remainingCapacity() 实现:
java
public int remainingCapacity() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return capacity - count;
} finally {
lock.unlock();
}
}
当 capacity == Integer.MAX_VALUE 时,capacity - count 始终为正(除非溢出,但 count 不会达到该值),因此 put 操作永远不会阻塞。
3.8 迭代器
LinkedBlockingDeque 提供两种迭代器:
iterator():正向,从first到last。descendingIterator():反向,从last到first。
它们都是弱一致性 的:迭代器创建后,对队列的修改不会反映在迭代器中(可能看到部分修改),但不会抛出 ConcurrentModificationException。实现上,迭代器在构造时获取锁并复制当前节点的引用,之后不再持有锁。
4. 必要流程
本节通过七个 Mermaid 图直观展示 LinkedBlockingDeque 的核心结构、交互流程与并发机制。
4.1 类图
文字描述 :
本类图展示了 LinkedBlockingDeque 的核心字段与方法。
- 核心字段 :
first和last分别指向双向链表的首尾节点;count记录元素数量;capacity是队列的最大容量(final不可变)。 - 并发控制 :
lock为单一的ReentrantLock,所有线程安全操作均需获取此锁;notEmpty和notFull是两个条件变量,分别用于阻塞等待非空和未满事件。 - 节点结构 :内部类
Node<E>包含元素引用item以及前后指针prev和next,构成双向链表的基础。 - 主要方法 :暴露了双端插入/移除的公有方法(如
putFirst、takeLast),内部调用私有的链接/解链方法(如linkFirst、unlinkFirst)完成具体节点操作。此类图清晰地揭示了LinkedBlockingDeque的组成与职责。
4.2 双向链表结构图
prev = null
next = B"] node2["Node B
prev = A
next = C"] node3["Node C
prev = B
next = null"] node1 -->|next| node2 node2 -->|next| node3 node2 -->|prev| node1 node3 -->|prev| node2 end
文字描述 :
该图描绘了一个包含三个节点(A、B、C)的 LinkedBlockingDeque 双向链表结构。
first指针指向首节点 A,其prev为null,next指向节点 B。last指针指向尾节点 C,其next为null,prev指向节点 B。- 中间节点 B 同时被 A 的
next和 C 的prev引用,实现了双向遍历能力。 - 当队列为空时,
first和last均为null;当仅有一个元素时,两者指向同一节点,且该节点的prev和next均为null。此图直观体现了双向链表的基本形态。
4.3 putFirst 和 takeFirst 交互时序图
文字描述 :
该时序图展示了队首插入与移除的阻塞交互过程。
- 消费者先执行 :消费者线程调用
takeFirst(),获取锁后发现队列为空(count == 0),于是调用notEmpty.await()释放锁并进入等待状态。 - 生产者插入 :生产者线程调用
putFirst(e),成功获取锁,检查容量未满后执行linkFirst(node)将新节点链接到队首,并使count变为 1。 - 唤醒消费者 :由于
count从 0 变为 1,条件满足,生产者调用notEmpty.signal()唤醒在notEmpty条件上等待的消费者线程。 - 消费者恢复 :消费者被唤醒后重新竞争锁,成功获取后执行
unlinkFirst()移除并返回元素,最后释放锁。整个过程体现了阻塞队列的经典等待-通知机制。
4.4 putLast 和 takeLast 交互时序图
文字描述 :
此图描述了队尾阻塞操作的对称流程。与队首操作类似,但方向相反:
- 消费者调用
takeLast()从队尾移除元素,队列空时在notEmpty条件上阻塞。 - 生产者调用
putLast(e)将元素链接到队尾,若队列由空变为非空,则通过notEmpty.signal()唤醒等待的消费者。 - 被唤醒的消费者执行
unlinkLast()完成移除。
由于LinkedBlockingDeque使用单锁,无论是队首还是队尾操作,锁的获取与条件等待机制完全对称,保证了双端操作的线程安全。
4.5 双端同时操作示意图(单锁串行化)
文字描述 :
该时序图强调了 LinkedBlockingDeque 单锁设计导致的串行化效应。
- 三个线程分别执行
putFirst、putLast、takeFirst,但由于所有操作共享同一把ReentrantLock,同一时刻只有一个线程能持有锁并执行操作。 - 图中线程 A 先获得锁,完成队首插入后释放锁;线程 B 紧接着获得锁执行队尾插入;最后线程 C 获得锁完成队首移除。
- 尽管这些操作分别作用于队首和队尾,在数据结构上互不冲突,但因为锁的独占性,它们必须串行执行。这是该队列与
LinkedBlockingQueue(入队出队可并发)在并发度上的根本区别。此设计简化了双端操作的并发控制,避免了复杂的死锁风险。
4.6 通用 unlink 移除节点流程图
p.next = n; n.prev = p"] J --> K["count--"] H --> K I --> K K --> L{"count == capacity-1 ?"} L -- "是" --> M["notFull.signal()"] L -- "否" --> N["释放锁 lock.unlock()"] M --> N N --> O["返回 true"] classDef default fill:#f9f9f9,stroke:#333,stroke-width:1px; classDef condition fill:#fff4e6,stroke:#ffa500,stroke-width:1.5px; class D,G,L condition; class E,O return; classDef return fill:#e8f5e9,stroke:#4caf50,stroke-width:1.5px; class E,O return;
文字描述 :
该流程图描述了 remove(Object o) 方法的内部执行路径,核心是调用 unlink(Node x) 通用解链逻辑。
- 加锁与查找 :方法首先获取锁,然后遍历链表(从
first开始)寻找第一个equals匹配的节点。若未找到则释放锁并返回false。 - 解链分支 :找到目标节点后,根据其位置分三种情况处理:
- 首节点 :直接调用
unlinkFirst()复用队首移除逻辑。 - 尾节点 :直接调用
unlinkLast()复用队尾移除逻辑。 - 中间节点 :手动更新前驱节点的
next和后继节点的prev,将目标节点从链中剥离。
- 首节点 :直接调用
- 计数与信号 :无论哪种情况,
count递减 1。随后检查队列是否从满变为未满(count == capacity - 1),若是则调用notFull.signal()唤醒一个等待插入的生产者线程。 - 解锁返回 :最后释放锁,返回
true。整个过程持有锁,保证了链表结构的完整性。
4.7 容量满时 putFirst 阻塞流程
文字描述 :
此流程图展示了当队列已满时,putFirst 操作如何进入阻塞等待,并在条件满足后恢复执行。
- 加锁后检查容量 :线程获取锁后,首先检查当前元素数量
count是否等于capacity。若未满,则直接调用linkFirst完成插入,并在插入后若count == 1(由空变非空)则唤醒等待的消费者。 - 满则阻塞 :若队列已满,线程调用
notFull.await()释放锁并进入条件等待队列,自身线程状态变为WAITING。 - 被唤醒后重新检查 :当其他线程(如消费者)调用
takeFirst或takeLast移除元素并发送notFull.signal()后,该线程从await()返回并重新竞争锁。获取锁后,它必须再次检查count == capacity,因为可能存在多个生产者同时被唤醒,或者唤醒后瞬间队列又被填满(虚假唤醒或竞争)。 - 循环直至成功 :只有当
count < capacity时,才跳出循环执行插入。这是条件等待的标准范式(while循环检查),确保了并发环境下的正确性。
以上即为根据 Mermaid 10.0.2 规范更新并添加详细文字描述后的第 4 部分内容。其余部分(概述、方法说明、源码分析、应用举例、性能分析、注意事项、对比总结等)保持不变,确保整篇文章的完整性和深度。
5. 实际应用场景与代码举例(JDK 8 兼容)
以下所有示例均为完整可编译运行的 Java 程序,基于 JDK 8。
5.1 工作窃取(Work Stealing)模拟
java
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class WorkStealingExample {
static class Worker implements Runnable {
private final int id;
private final LinkedBlockingDeque<Runnable> deque;
private final AtomicInteger taskCounter;
public Worker(int id, LinkedBlockingDeque<Runnable> deque, AtomicInteger counter) {
this.id = id;
this.deque = deque;
this.taskCounter = counter;
}
@Override
public void run() {
try {
while (taskCounter.get() > 0 || !deque.isEmpty()) {
Runnable task = deque.pollFirst(100, TimeUnit.MILLISECONDS);
if (task == null) {
// 窃取:从队尾偷一个任务
task = deque.pollLast();
if (task != null) {
System.out.println("Worker " + id + " stole a task from tail");
}
} else {
System.out.println("Worker " + id + " took from head");
}
if (task != null) {
task.run();
taskCounter.decrementAndGet();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public static void main(String[] args) throws InterruptedException {
final int CAPACITY = 100;
final int TASK_COUNT = 20;
LinkedBlockingDeque<Runnable> deque = new LinkedBlockingDeque<>(CAPACITY);
AtomicInteger counter = new AtomicInteger(TASK_COUNT);
// 添加任务,每个任务模拟耗时操作
for (int i = 0; i < TASK_COUNT; i++) {
final int taskId = i;
deque.putLast(() -> {
System.out.println("Executing task " + taskId + " on " + Thread.currentThread().getName());
try { Thread.sleep(50); } catch (InterruptedException e) {}
});
}
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 1; i <= 4; i++) {
executor.execute(new Worker(i, deque, counter));
}
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
}
}
5.2 双端生产者-消费者(FIFO + LIFO)
java
import java.util.concurrent.*;
public class DualConsumerExample {
public static void main(String[] args) throws InterruptedException {
LinkedBlockingDeque<String> deque = new LinkedBlockingDeque<>(10);
// 生产者
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
deque.putLast("Task-" + i);
System.out.println("Produced: Task-" + i);
Thread.sleep(100);
} catch (InterruptedException e) { break; }
}
}).start();
// FIFO 消费者(头部消费)
new Thread(() -> {
while (true) {
try {
String task = deque.takeFirst();
System.out.println("FIFO Consumer took: " + task);
} catch (InterruptedException e) { break; }
}
}).start();
// LIFO 消费者(尾部消费)
new Thread(() -> {
while (true) {
try {
String task = deque.takeLast();
System.out.println("LIFO Consumer took: " + task);
} catch (InterruptedException e) { break; }
}
}).start();
}
}
5.3 用作阻塞栈(LIFO)
java
import java.util.concurrent.*;
public class BlockingStackExample {
public static void main(String[] args) throws InterruptedException {
LinkedBlockingDeque<Integer> stack = new LinkedBlockingDeque<>(5);
// 压栈
for (int i = 0; i < 5; i++) {
stack.putFirst(i);
System.out.println("Pushed: " + i);
}
// 出栈
while (!stack.isEmpty()) {
int val = stack.takeFirst();
System.out.println("Popped: " + val);
}
}
}
5.4 有界容量控制示例
java
import java.util.concurrent.*;
public class BoundedCapacityDemo {
public static void main(String[] args) throws InterruptedException {
LinkedBlockingDeque<String> deque = new LinkedBlockingDeque<>(3);
// 非阻塞插入
System.out.println("offerFirst A: " + deque.offerFirst("A"));
System.out.println("offerFirst B: " + deque.offerFirst("B"));
System.out.println("offerFirst C: " + deque.offerFirst("C"));
System.out.println("offerFirst D: " + deque.offerFirst("D")); // false
// 阻塞插入
new Thread(() -> {
try {
System.out.println("Trying putFirst E...");
deque.putFirst("E");
System.out.println("Inserted E");
} catch (InterruptedException e) {}
}).start();
Thread.sleep(1000);
System.out.println("Taking from tail: " + deque.takeLast()); // 移除一个元素,释放空间
}
}
5.5 迭代器使用(弱一致性)
java
import java.util.Iterator;
import java.util.concurrent.*;
public class IteratorWeakConsistency {
public static void main(String[] args) throws InterruptedException {
LinkedBlockingDeque<Integer> deque = new LinkedBlockingDeque<>();
deque.putLast(1);
deque.putLast(2);
deque.putLast(3);
Iterator<Integer> it = deque.iterator();
// 迭代过程中修改队列
new Thread(() -> {
deque.putLast(4);
deque.pollFirst();
}).start();
while (it.hasNext()) {
System.out.println("Iter: " + it.next());
Thread.sleep(100);
}
// 反向迭代
Iterator<Integer> descIt = deque.descendingIterator();
while (descIt.hasNext()) {
System.out.println("Desc Iter: " + descIt.next());
}
}
}
6. 吞吐量与性能分析
6.1 单锁设计对吞吐量的影响
| 指标 | LinkedBlockingDeque | LinkedBlockingQueue | 备注 |
|---|---|---|---|
| 并发度 | 低(所有操作串行) | 高(入队与出队可并行) | 在纯 FIFO 场景下,LBQ 吞吐量约为 LBD 的 2-3 倍(据并发测试) |
| 锁竞争 | 激烈,尤其混合操作 | 分散,仅入队间竞争或出队间竞争 | |
| 适用场景 | 需要双端操作的场合 | 仅需单端 FIFO |
建议 :如果业务仅需 FIFO 队列,优先使用 LinkedBlockingQueue;若需工作窃取或双端阻塞操作,LinkedBlockingDeque 是唯一选择。
6.2 链表内存开销分析
每个节点包含三个引用(item、prev、next),以 64 位 JVM(压缩指针开启)为例:
- 对象头:12 字节(Mark Word 8 + Klass 4)
- 三个引用:3 × 4 = 12 字节(压缩后)
- 总计约 24 字节/节点,加上元素本身。
对比:
ArrayBlockingQueue:仅数组存储元素引用,内存紧凑,无额外节点开销。LinkedBlockingQueue:节点只有item和next,约 16 字节/节点。
6.3 容量可选的影响
- 无界 (
capacity = Integer.MAX_VALUE):put永不阻塞,但可能引发 OOM,需做好流量控制。 - 有界 :提供背压机制,
put会阻塞,触发上下文切换和条件等待,增加延迟但保护内存。
6.4 性能调优建议
- 合理设置容量:避免默认无界导致内存溢出,也避免过小导致频繁阻塞。
- 避免混合双端操作与单端操作 :若只使用一端,切换为
LinkedBlockingQueue或ArrayBlockingQueue。 - 批量操作使用
drainTo:drainTo在持有锁时一次性转移多个元素,效率远高于循环poll。 - 监控队列大小 :通过
size()定期检查,提前预警。
7. 注意事项与常见陷阱
| 陷阱 | 说明 | 规避措施 |
|---|---|---|
| 容量默认无界 | new LinkedBlockingDeque() 创建无界队列,大量插入可能导致 OOM |
务必指定合理容量 |
size() 非 O(1) 误区 |
有人认为链表 size() 需遍历,实则 count 字段由锁保护,size() 为 O(1) |
放心使用 |
remove(Object) 遍历开销 |
需遍历链表,O(n),且持有锁,阻塞其他操作 | 避免频繁使用,或使用 drainTo 配合集合操作 |
| 迭代器弱一致性 | 迭代过程中队列变化不会抛异常,但结果可能过期 | 接受弱一致性,或遍历前拷贝快照 |
drainTo 仅从头部转移 |
内部调用 pollFirst,无法从尾部转移 |
若需尾部转移,需手动循环 pollLast |
null 元素禁止 |
插入 null 立即抛出 NullPointerException |
编码时做好非空校验 |
| 单锁导致死锁? | 内部操作不会死锁,但若在 put 等待条件时中断,需正确处理 InterruptedException |
捕获并恢复中断状态 |
put(e) 等价于 putLast(e) |
队列操作默认尾部插入 | 符合直觉,但需注意与其他双端方法混用时的顺序 |
| 内存可见性 | 所有字段修改均在锁内完成,保证可见性 | 无需额外 volatile |
与 ConcurrentLinkedDeque 混淆 |
后者非阻塞,无容量限制,使用 CAS | 需要阻塞等待时选用 LinkedBlockingDeque |
8. 与其他阻塞队列的对比总结
| 特性 | LinkedBlockingDeque | LinkedBlockingQueue | ArrayBlockingQueue | LinkedTransferQueue |
|---|---|---|---|---|
| 双端操作 | 支持 | 不支持 | 不支持 | 不支持(单向) |
| 有界性 | 可选有界(默认无界) | 可选有界(默认无界) | 固定有界 | 无界 |
| 数据结构 | 双向链表 | 单向链表 | 数组 | 双向链表(节点含更多字段) |
| 锁机制 | 单锁 (ReentrantLock) |
双锁(分离) | 单锁 | 无锁(CAS + 自旋) |
| size() 复杂度 | O(1) | O(1) | O(1) | O(n) (遍历计数) |
| 支持 transfer | 否 | 否 | 否 | 是(transfer、tryTransfer) |
| 典型应用场景 | 工作窃取、双端阻塞队列/栈 | 常规生产者-消费者 | 固定大小缓冲区、资源池 | 高吞吐量、直接交接模式 |
| 内存占用 | 较高(双指针节点) | 中等(单指针节点) | 低(数组紧凑) | 较高(节点含更多字段) |
9. 总结与学习指引
核心特点回顾
- 可选有界 :
capacity参数控制,默认无界需警惕。 - 双向链表:支持高效的队首/队尾操作,内存开销略高。
- 双端阻塞操作 :实现了
BlockingDeque,既可当队列也可当栈。 - 单锁并发控制 :简化设计,保证正确性,但并发吞吐量低于
LinkedBlockingQueue。
使用建议
- 需要双端阻塞操作 (如工作窃取、双端消费)时,选择
LinkedBlockingDeque。 - 纯 FIFO 高并发场景 ,优先
LinkedBlockingQueue(双锁)以获得更高吞吐量。 - 固定容量且要求内存紧凑 ,考虑
ArrayBlockingQueue。 - 非阻塞双端操作 ,可使用
ConcurrentLinkedDeque。 - 务必设置合理容量,并监控队列大小,防止无界导致的内存问题。