1. 概述
LinkedBlockingQueue 是 Java 并发包(java.util.concurrent)中提供的一个可选有界 的阻塞队列实现,其底层基于单向链表 组织元素,严格遵循 FIFO(先进先出) 顺序。它是阻塞队列系列中应用最为广泛的实现之一,尤其在生产者-消费者模型和高并发场景中扮演着关键角色。
核心特点一览
| 特性 | 说明 |
|---|---|
| 数据结构 | 单向链表,每个元素被包装为 Node 节点,通过 next 指针串联 |
| 有界/无界 | 可选有界 。无参构造器默认容量为 Integer.MAX_VALUE,可视为"无界" |
| 锁机制 | 双锁分离 :putLock 管理入队操作,takeLock 管理出队操作,大幅提升并发吞吐量 |
| 公平性 | 不支持公平参数。双锁设计下难以实现公平调度,实践中饥饿概率较低 |
| 内存分配 | 动态分配节点,每次插入创建新 Node 对象;使用哨兵节点(dummy head)简化边界判断 |
| 容量计数 | 使用 AtomicInteger 维护元素个数,保证跨锁可见性,实现轻量级竞争 |
| 所属包 | java.util.concurrent |
典型应用场景
- 生产者-消费者模型:解耦生产线程和消费线程,利用阻塞特性实现流量削峰。
- 线程池任务队列 :
Executors.newFixedThreadPool()和Executors.newSingleThreadExecutor()默认使用LinkedBlockingQueue作为任务缓冲队列(无界)。 - 消息中间件缓冲层:如日志异步处理、事件分发器等。
与 ArrayBlockingQueue 的主要区别(先睹为快)
| 对比维度 | LinkedBlockingQueue | ArrayBlockingQueue |
|---|---|---|
| 锁结构 | 双锁分离(putLock + takeLock) |
单锁(一把 ReentrantLock 管理入队和出队) |
| 内存结构 | 链表,动态分配节点 | 数组,预分配连续内存 |
| 有界性默认值 | 无参构造容量 = Integer.MAX_VALUE(近似无界) |
必须显式指定容量,无默认无界构造器 |
| 公平性 | 不支持 | 支持公平锁(fair 参数) |
| 内存占用 | 较高,每个元素需额外存储 Node 对象头和 next 指针 |
较低,仅数组引用 |
| GC 压力 | 较大,节点频繁创建与回收 | 较小,数组元素覆盖 |
| 吞吐量特征 | 高并发下表现更优,生产者和消费者可并发操作 | 竞争激烈时性能下降明显 |
本文将深入 JDK 8 源码,逐层剖析 LinkedBlockingQueue 的设计思想与实现细节,并与 ArrayBlockingQueue 进行对比,帮助你建立全面的阻塞队列知识体系。
2. 核心方法说明
以下是 LinkedBlockingQueue 主要 API 的速查表,涵盖了构造、插入、移除、检查、批量操作等核心方法。
| 方法 | 参数 | 返回值 | 阻塞行为 | 异常 |
|---|---|---|---|---|
LinkedBlockingQueue() |
无 | 构造器,容量 = Integer.MAX_VALUE |
无 | 无 |
LinkedBlockingQueue(int capacity) |
capacity:队列容量(必须 > 0) |
构造器 | 无 | IllegalArgumentException 若 capacity <= 0 |
LinkedBlockingQueue(Collection<? extends E> c) |
初始集合 | 构造器,容量 = Integer.MAX_VALUE,添加集合元素 |
无 | NullPointerException 若集合或元素为 null |
put(E e) |
e:元素 |
void |
队列满时阻塞,直到有空间 | InterruptedException、NullPointerException |
offer(E e) |
e:元素 |
boolean:成功 true,队列满返回 false |
不阻塞 | NullPointerException |
offer(E e, long timeout, TimeUnit unit) |
元素、超时时间、时间单位 | boolean:成功 true,超时后仍满返回 false |
等待指定时间 | InterruptedException、NullPointerException |
take() |
无 | E:队首元素 |
队列空时阻塞,直到有元素 | InterruptedException |
poll() |
无 | E:队首元素,空返回 null |
不阻塞 | 无 |
poll(long timeout, TimeUnit unit) |
超时时间、时间单位 | E:元素,超时后仍空返回 null |
等待指定时间 | InterruptedException |
peek() |
无 | E:队首元素(不移除),空返回 null |
不阻塞 | 无 |
size() |
无 | int:当前元素个数(AtomicInteger,弱一致性) |
无 | 无 |
remainingCapacity() |
无 | int:剩余容量(容量 - 当前个数) |
无 | 无 |
drainTo(Collection<? super E> c) |
目标集合 | int:转移的元素数量 |
无(但分批持锁) | NullPointerException 若集合为 null |
drainTo(Collection<? super E> c, int maxElements) |
目标集合、最大转移数 | int:实际转移数 |
无(分批持锁) | NullPointerException |
注意 :
size()返回的是AtomicInteger的瞬时值,由于双锁设计,count可能在调用后立即变化,因此具有弱一致性 。remainingCapacity()在无界队列下总是返回Integer.MAX_VALUE - count,意义有限。
3. 核心原理与源码分析(基于 JDK 8)
3.1 数据结构与核心字段
LinkedBlockingQueue 内部通过一个静态内部类 Node 构建单向链表,同时维护了两个独立的锁和对应的条件队列。
java
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
// 链表节点定义
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
// 容量限制(final,一经设定不可变)
private final int capacity;
// 当前元素个数(AtomicInteger 保证跨锁可见性和原子更新)
private final AtomicInteger count = new AtomicInteger();
// 链表头指针(始终指向哨兵节点,其 item 为 null)
transient Node<E> head;
// 链表尾指针(指向最后一个节点,其 next 为 null)
private transient Node<E> last;
// 消费者锁(出队操作使用)
private final ReentrantLock takeLock = new ReentrantLock();
// 非空条件(消费者等待)
private final Condition notEmpty = takeLock.newCondition();
// 生产者锁(入队操作使用)
private final ReentrantLock putLock = new ReentrantLock();
// 非满条件(生产者等待)
private final Condition notFull = putLock.newCondition();
}
字段详解:
head:始终指向一个 哨兵节点 (dummy node),其item == null。第一个有效元素存放在head.next中。这一设计极大简化了边界条件判断------出队时无需检查head是否为null,只需获取head.next即可。last:指向链表最后一个节点。入队时直接追加到last.next,然后更新last。count:使用AtomicInteger而非volatile int。因为入队和出队使用不同的锁,count的更新可能在两把锁的保护下分别进行,需要保证原子性和跨锁可见性。AtomicInteger完美满足这一需求,且 CAS 操作比锁开销更小。takeLock/putLock:两把独立的ReentrantLock,分别保护出队和入队操作。这是LinkedBlockingQueue高并发性能的核心支柱。notEmpty/notFull:分别绑定在takeLock和putLock上的条件队列,用于线程间的等待/唤醒协调。
3.2 Node 内部类设计
java
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
节点设计极简,仅包含数据 item 和后继指针 next。没有前驱指针 ,因为队列仅支持 FIFO 的单向遍历,无需反向操作。这也意味着 LinkedBlockingQueue 的 remove(Object) 操作需要从头遍历链表,时间复杂度 O(n)。
3.3 构造器源码分析
① 无参构造器
java
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
直接调用有参构造器,传入最大容量 Integer.MAX_VALUE(约 21 亿),实际上近似于"无界"。
② 指定容量构造器
java
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null); // 创建哨兵节点
}
这里创建了哨兵节点,并让 head 和 last 都指向它。此时队列为空,head.next == null。
③ 从集合初始化的构造器
java
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // 锁定,防止并发干扰
try {
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
持有 putLock 批量入队,避免并发问题。注意容量默认为 Integer.MAX_VALUE,但若集合元素数量超过该值会抛异常(实际几乎不可能)。
3.4 put / take 核心流程:双锁分离与级联唤醒
这是 LinkedBlockingQueue 最精彩的部分。我们通过源码逐行剖析。
put(E e) 实现
java
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
// 1. 若队列已满,则在 notFull 条件上等待
while (count.get() == capacity) {
notFull.await();
}
// 2. 入队操作
enqueue(node);
// 3. 获取入队前的元素个数,并将 count 加 1
c = count.getAndIncrement();
// 4. 如果入队后仍有空间,则唤醒其他等待的生产者
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
// 5. 若入队前队列为空(c == 0),则唤醒可能等待的消费者
if (c == 0)
signalNotEmpty();
}
take() 实现
java
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
// 1. 若队列为空,则在 notEmpty 条件上等待
while (count.get() == 0) {
notEmpty.await();
}
// 2. 出队操作
x = dequeue();
// 3. 获取出队前的元素个数,并将 count 减 1
c = count.getAndDecrement();
// 4. 如果出队后仍有元素,则唤醒其他等待的消费者
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
// 5. 若出队前队列已满(c == capacity),则唤醒可能等待的生产者
if (c == capacity)
signalNotFull();
return x;
}
级联唤醒机制分析
两个方法在释放锁之后,都额外进行了一次条件唤醒:
put中:若c == 0(入队前队列为空),则调用signalNotEmpty()唤醒消费者。take中:若c == capacity(出队前队列已满),则调用signalNotFull()唤醒生产者。
为什么要在释放锁之后唤醒?
为了减少锁竞争。signalNotEmpty() 需要获取 takeLock,如果还在持有 putLock 的状态下执行,就会形成锁嵌套 ,不仅降低并发度,还可能增加死锁风险。JDK 的设计者将唤醒动作推迟到释放 putLock 之后,使得生产者线程尽早释放锁,消费者线程可立即竞争 takeLock,从而提升吞吐量。
为什么只在边界条件时唤醒?
如果每次 put 都唤醒消费者,会带来大量无效唤醒(例如队列未满时消费者本来就不会阻塞)。仅在 c == 0 时唤醒,表明队列从空变为非空 ,此时才可能有消费者正在等待。同理,仅在 c == capacity 时唤醒生产者。这种精确唤醒策略大幅减少了不必要的上下文切换。
3.5 offer / poll 非阻塞版本
offer(E e) 和 poll() 是 put/take 的非阻塞版本。
java
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity)
return false; // 快速失败,无需加锁
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return c >= 0;
}
注意在加锁前先进行了一次 count.get() == capacity 的快速检查 。这是一种无锁优化,能减少不必要的加锁开销,但即便检查通过,加锁后仍需再次验证(因为并发环境下 count 可能已被改变)。若加锁后发现队列已满,则返回 false。成功时的唤醒逻辑与 put 一致。
poll() 实现完全对称。
3.6 超时版本 offer / poll
利用 Condition.awaitNanos(long nanosTimeout) 实现限时等待。
java
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
// ...
long nanos = unit.toNanos(timeout);
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
enqueue(node);
// ... 唤醒逻辑同 offer
} finally {
putLock.unlock();
}
// ...
}
awaitNanos() 返回剩余等待时间(纳秒),若超时则返回 ≤ 0。循环检查保证了即使被虚假唤醒也能正确重试。
3.7 链表操作详解:enqueue 与 dequeue
enqueue(Node node)
java
private void enqueue(Node<E> node) {
// 经典的单链表尾插法
last = last.next = node;
}
等价于:
last.next = node;last = node;
由于有哨兵节点,初始时 head == last,第一次入队时 head.next = node,last 指向新节点。
dequeue()
java
private E dequeue() {
Node<E> h = head; // 哨兵节点
Node<E> first = h.next; // 真正的第一个元素
h.next = h; // help GC
head = first; // first 成为新的哨兵
E x = first.item;
first.item = null; // 新哨兵的 item 为 null
return x;
}
关键细节 h.next = h 的作用:将旧哨兵节点的 next 指向自身,形成一个自引用环 。这样一来,旧哨兵节点不再被任何外部引用可达(head 已指向新哨兵),且其内部也没有指向其他有效节点的引用,有助于 GC 快速回收。尽管现代 JVM 的 GC 已足够智能,但这种显式辅助仍是良好实践。
3.8 双锁设计精髓总结
- 并发度倍增 :生产者和消费者可以同时操作队列(一个在尾部插入,一个在头部移除),互不干扰。只有在操作
count时存在轻量级的 CAS 竞争。 AtomicInteger作为桥梁 :count是连接两把锁的共享变量,使用 CAS 保证原子性,同时其volatile语义(AtomicInteger内部使用volatile修饰 value)确保跨锁的可见性。- 精确唤醒 :仅在队列从空变非空 或从满变非满时跨锁唤醒对方线程,避免无效竞争。
- 无公平性:由于两把锁独立,实现全局公平调度异常复杂,且对于大多数高并发场景,吞吐量优先的非公平策略更为合适。
3.9 容量上限风险警示
当 capacity = Integer.MAX_VALUE 时,put 操作几乎永远不会阻塞(除非系统内存耗尽)。这在生产者速率持续高于消费者时,会导致队列无限增长,最终引发 OutOfMemoryError。生产环境中,务必根据实际内存和业务承载能力设置合理的有界容量。
4. 必要流程的 Mermaid 图
4.1 类图
展示 LinkedBlockingQueue 的核心字段和方法,并与 ArrayBlockingQueue 进行对比。
对比描述 :LinkedBlockingQueue 使用链表和两把锁,字段包含 head/last 指针和独立的锁与条件;而 ArrayBlockingQueue 使用数组和单把 lock,包含 takeIndex/putIndex 等环形数组索引。
4.2 链表结构图(包含三个元素)
(哨兵节点)
item=null"] --> node1["Node1
item=A
next→"] node1 --> node2["Node2
item=B
next→"] node2 --> node3["Node3
item=C
next=null"] last["last"] --> node3 end
- 哨兵节点始终位于队首,
head指向它。 - 第一个有效元素在
head.next。 last指向最后一个节点,其next为null。
4.3 双锁工作示意图
put(E)"] -->|竞争| putLock["putLock
(入队锁)"] Producer2["生产者线程2
put(E)"] -->|竞争| putLock putLock -->|允许一个线程| TailOps["操作 last
enqueue"] Consumer1["消费者线程1
take()"] -->|竞争| takeLock["takeLock
(出队锁)"] Consumer2["消费者线程2
take()"] -->|竞争| takeLock takeLock -->|允许一个线程| HeadOps["操作 head
dequeue"] TailOps -.->|更新 count| AtomicCount["AtomicInteger count
(CAS 操作)"] HeadOps -.->|更新 count| AtomicCount end style putLock fill:#f9f,stroke:#333,stroke-width:2px style takeLock fill:#bbf,stroke:#333,stroke-width:2px
- 生产者和消费者分别持有不同的锁,可同时进行入队和出队操作。
- 只有在更新
count时才会发生轻量级的 CAS 竞争(非阻塞)。
4.4 非阻塞 offer/poll 快速路径流程图
为清晰展示两个非阻塞方法的执行路径,分别绘制 offer(E e) 和 poll() 流程图。
① offer(E e) 流程图
无锁快速检查} FastCheck -- 是 --> ReturnFalse1[返回 false] FastCheck -- 否 --> LockPut[获取 putLock] LockPut --> CheckAgain{count < capacity ?} CheckAgain -- 否 --> UnlockFalse[释放 putLock
返回 false] CheckAgain -- 是 --> Enqueue[执行 enqueue 入队] Enqueue --> GetCount[ c = count.getAndIncrement ] GetCount --> CheckSignalSelf{ c + 1 < capacity ? } CheckSignalSelf -- 是 --> SignalNotFull[notFull.signal
唤醒其他生产者] CheckSignalSelf -- 否 --> UnlockPut[释放 putLock] SignalNotFull --> UnlockPut UnlockPut --> CheckEmptyBoundary{ c == 0 ? } CheckEmptyBoundary -- 是 --> SignalNotEmpty[signalNotEmpty
唤醒消费者] CheckEmptyBoundary -- 否 --> ReturnTrue[返回 true] SignalNotEmpty --> ReturnTrue
② poll() 流程图
无锁快速检查} FastCheck -- 是 --> ReturnNull1[返回 null] FastCheck -- 否 --> LockTake[获取 takeLock] LockTake --> CheckAgain{count > 0 ?} CheckAgain -- 否 --> UnlockNull[释放 takeLock
返回 null] CheckAgain -- 是 --> Dequeue[执行 dequeue 出队] Dequeue --> GetCount[ c = count.getAndDecrement ] GetCount --> CheckSignalSelf{ c > 1 ? } CheckSignalSelf -- 是 --> SignalNotEmptySelf[notEmpty.signal
唤醒其他消费者] CheckSignalSelf -- 否 --> UnlockTake[释放 takeLock] SignalNotEmptySelf --> UnlockTake UnlockTake --> CheckFullBoundary{ c == capacity ? } CheckFullBoundary -- 是 --> SignalNotFull[signalNotFull
唤醒生产者] CheckFullBoundary -- 否 --> ReturnItem[返回元素] SignalNotFull --> ReturnItem
流程图详细文字描述
offer 流程描述:
- 快速无锁检查 :首先不获取锁检查
count是否等于capacity,若满则立即返回false(此优化减少加锁开销)。 - 获取 putLock:如果可能不满,获取生产者锁。
- 双重检查:加锁后再次检查容量,防止并发导致的状态变化。
- 入队与计数更新 :执行
enqueue,并通过getAndIncrement获取入队前的计数值c。 - 条件唤醒(生产者侧) :若入队后队列仍有余量(
c+1 < capacity),则唤醒一个等待在notFull上的其他生产者。 - 释放生产者锁。
- 边界唤醒(消费者侧) :若入队前队列为空(
c == 0),说明可能有消费者正在等待,跨锁调用signalNotEmpty唤醒消费者。 - 返回 true。
poll 流程描述:
- 快速无锁检查 :检查
count是否为 0,若空则返回null。 - 获取 takeLock:若可能非空,获取消费者锁。
- 双重检查:加锁后再次检查队列是否为空。
- 出队与计数更新 :执行
dequeue,通过getAndDecrement获取出队前的计数值c。 - 条件唤醒(消费者侧) :若出队后仍有元素(
c > 1),唤醒其他等待的消费者。 - 释放消费者锁。
- 边界唤醒(生产者侧) :若出队前队列已满(
c == capacity),说明可能有生产者正在等待空间,跨锁调用signalNotFull唤醒生产者。 - 返回被移除的元素。
4.5 阻塞 put/take 完整交互时序图
场景一:队列空,take 先阻塞
流程描述:
-
初始状态 :队列为空,
count.get() == 0。此时一个消费者线程(Consumer)调用take()准备获取元素。 -
消费者阻塞:
Consumer调用takeLock.lockInterruptibly()获取消费者锁,成功后进入临界区。- 检查
count.get() == 0,确认队列为空。 - 调用
notEmpty.await()。此操作会原子地 释放takeLock并将当前线程挂起到notEmpty条件队列上,消费者进入阻塞状态。
-
生产者入队:
- 随后,一个生产者线程(
Producer)调用put(e)插入元素。 Producer获取putLock(与takeLock无关,因此可以独立进行),成功后进入临界区。- 由于队列有空余(容量未满),直接执行
enqueue(node),将新节点追加到链表尾部。 - 执行
c = count.getAndIncrement(),获取入队前的元素个数。因为队列原本为空,所以c == 0。 - 入队后队列未满(通常如此,除非容量为 1),
Producer会调用notFull.signal()唤醒其他可能等待的生产者(本例中无)。 - 释放
putLock。
- 随后,一个生产者线程(
-
边界唤醒消费者:
Producer在释放putLock后,检查到c == 0(入队前队列为空),表明队列由空变为非空,此时极有可能有消费者在等待。- 调用
signalNotEmpty()方法。该方法内部会获取takeLock,然后调用notEmpty.signal()唤醒一个等待的消费者,最后释放takeLock。 - 注意:此唤醒操作发生在释放
putLock之后,避免了锁嵌套,提高了并发效率。
-
消费者被唤醒并完成出队:
- 被唤醒的
Consumer从await()调用中返回,并自动重新获取takeLock(await方法在返回前会重新竞争锁)。 - 重新获得锁后,
Consumer从while (count.get() == 0)循环中退出(因为此时count已变为 1)。 - 执行
dequeue()从链表头部移除元素。 - 执行
c = count.getAndDecrement(),获取出队前的元素个数(此时c == 1)。 - 由于出队后队列仍可能非空(
c > 1时),Consumer会调用notEmpty.signal()唤醒其他可能等待的消费者,形成传播唤醒。 - 释放
takeLock。
- 被唤醒的
-
边界检查:
Consumer在释放锁后检查c == capacity(出队前队列是否已满)。在本场景中c == 1且容量大于 1,因此条件不成立,不会调用signalNotFull()。- 最终
Consumer返回获取的元素,take()调用结束。
通过上述步骤,LinkedBlockingQueue 利用精确的边界唤醒 (只在 c == 0 时唤醒消费者,只在 c == capacity 时唤醒生产者)避免了无效的信号风暴,并结合双锁分离实现了高并发下的高效协作。
场景二:队列满,put 先阻塞
流程描述
- 初始状态 :队列已满,
count.get() == capacity。此时有多个生产者线程(示例中为Producer1和Producer2)尝试调用put()插入元素。 - 生产者阻塞 :
Producer1获取putLock后检查队列状态,发现已满,于是调用notFull.await()释放锁并阻塞。Producer2同样获取putLock并因队列满而阻塞在notFull条件上。- 此时
notFull的条件等待队列中可能存在多个生产者线程。
- 消费者出队触发唤醒 :
- 一个消费者线程(
Consumer)调用take()获取takeLock,执行dequeue()移除一个元素。 - 出队前计数值
c = count.getAndDecrement()等于capacity,表明队列由满变为非满。 - 消费者在释放
takeLock后,调用signalNotFull()跨锁唤醒一个等待在notFull上的生产者(通常是等待最久的那个,具体取决于 AQS 的唤醒策略,非公平锁下不保证严格 FIFO)。
- 一个消费者线程(
- 生产者被唤醒并完成入队 :
Producer1从await()返回后,会重新竞争putLock。获得锁后继续执行入队操作(enqueue)。- 入队后
c = count.getAndIncrement()返回入队前的计数值(此时应为capacity - 1)。 - 若入队后队列仍未满(
c + 1 < capacity),Producer1会调用notFull.signal()唤醒下一个阻塞的生产者(例如Producer2),形成传播唤醒,直到队列再次满或没有等待者为止。
- 级联唤醒的优雅性:这种设计确保了只要队列中有空间,就会尽可能多地激活生产者,避免线程在不必要时继续阻塞,同时避免了生产者侧的条件队列"饿死"。
4.6 超时 offer 的等待-超时恢复流程
流程说明:
-
参数预处理与超时计算
方法首先检查元素是否为
null,若是则抛出NullPointerException。然后将用户传入的时间单位转换为纳秒:
long nanos = unit.toNanos(timeout); -
获取生产者锁(可中断)
调用
putLock.lockInterruptibly()获取锁。若线程在等待锁期间被中断,则抛出InterruptedException。 -
循环检查队列状态与超时
进入
try块后,使用while (count.get() == capacity)循环持续判断队列是否已满:-
若队列未满,跳出循环,执行入队操作。
-
若队列已满,则检查剩余等待时间
nanos:- 若
nanos <= 0,表示已超时,直接返回false。 - 否则,调用
nanos = notFull.awaitNanos(nanos)进入限时等待状态。
- 若
-
-
awaitNanos的行为
awaitNanos会使当前线程释放putLock并挂起,直到以下四种情况之一发生:- 被唤醒 (通过
signal或signalAll)。 - 超时(到达指定等待时间)。
- 被中断 (抛出
InterruptedException)。 - 虚假唤醒 (spurious wakeup)。
方法返回值是剩余等待时间 (timeout - 实际等待时长),单位纳秒。若返回值 ≤ 0,表明已超时。
- 被唤醒 (通过
-
超时后重试逻辑
由于可能存在虚假唤醒,线程被唤醒后必须再次检查 队列状态。循环将使用更新后的
nanos继续判断:- 若此时队列仍满且
nanos > 0,则继续等待剩余时间。 - 若
nanos <= 0,则退出循环,最终在finally中释放锁并返回false。
- 若此时队列仍满且
-
成功入队的后续处理
若队列出现空位(无论是因为被消费者唤醒还是刚好超时前有空位),则跳出循环,执行:
enqueue(node)将元素加入链表尾部。c = count.getAndIncrement()获取入队前计数。- 若入队后队列仍未满(
c + 1 < capacity),则调用notFull.signal()唤醒其他可能等待的生产者。 - 释放
putLock。
-
边界唤醒消费者
在释放锁后,若入队前队列为空(
c == 0),则调用signalNotEmpty()跨锁唤醒消费者,确保因队列空而阻塞的消费者能够及时获取新元素。 -
返回结果
最终方法返回
true,表示元素成功入队。
与 ArrayBlockingQueue 对比 :
ArrayBlockingQueue 的 offer 超时版本同样使用 awaitNanos,但由于其单锁设计,入队和出队竞争同一把锁,超时等待期间其他线程无法进行任何操作。而 LinkedBlockingQueue 中,超时等待仅阻塞持有 putLock 的生产者,消费者仍可并发地通过 takeLock 出队,从而减少了超时等待线程对整体吞吐量的影响。
4.7 drainTo 批量消费内部循环流程图
流程说明:
-
参数校验
首先检查目标集合
c是否为null,若为null则抛出NullPointerException。同时检查c是否为当前队列本身(会导致死循环),若是则抛出IllegalArgumentException。若
maxElements <= 0,则直接返回0(不转移任何元素)。 -
获取消费者锁
调用
takeLock.lock()获取消费者锁。注意,此处使用不可中断的lock(),因为批量转移过程通常较快,中断支持意义不大,且保持代码简洁。 -
确定实际转移数量
计算本次最多可转移的元素个数:
int n = Math.min(maxElements, count.get());其中
count.get()是当前队列中的元素总数。由于已持有takeLock,此值在后续循环中不会因消费者并发而增加(但生产者可能入队,不过入队操作不修改head区域,因此不影响遍历)。 -
循环执行出队并添加到目标集合
使用
for循环执行n次:- 调用
dequeue()方法移除队首元素。注意:dequeue内部会更新head指针,并处理哨兵节点的自引用以帮助 GC。 - 将返回的元素
E x通过c.add(x)添加到目标集合。 - 若在添加过程中目标集合抛出异常(如集合不支持添加元素、类型不匹配等),则已转移的元素将保留在集合中,但循环会中断,并抛出相应异常。同时,队列中已移除的元素不可恢复,这符合
drainTo的"尽力而为"语义。
- 调用
-
原子更新队列计数
循环结束后,调用
count.getAndAdd(-n)原子地将count减去实际转移的元素数量n。注意:即使循环中途因异常退出,
n也仅代表实际成功转移的数量(可通过循环计数器精确控制),因此count的更新是准确的。 -
唤醒可能等待的生产者
在释放锁之前,检查转移前的队列是否已满:
- 若转移前
count == capacity(即队列满),说明生产者可能正阻塞在notFull条件上。由于已经移除了n个元素,队列由满变为非满,因此需要调用signalNotFull()唤醒一个或多个生产者(具体唤醒数量取决于n的大小,但 JDK 8 中简单调用一次signal即可,因为生产者会在获取锁后重新检查容量,如有余量会级联唤醒其他生产者)。
- 若转移前
-
释放消费者锁
在
finally块中释放takeLock。 -
返回转移数量
方法返回实际转移的元素个数
n。
与 ArrayBlockingQueue 的差异分析:
- 锁持有范围 :
LinkedBlockingQueue的drainTo仅持有takeLock,因此在批量转移过程中,生产者可以继续入队 (除非队列满需等待),极大地降低了对生产者线程的阻塞影响。而ArrayBlockingQueue的drainTo持有单把全局锁,转移期间生产者完全无法入队。 - 批量操作效率 :两者都通过单次加锁完成多个元素的移除,相比循环
poll大幅减少了锁获取和释放的开销。但LinkedBlockingQueue由于双锁分离,其drainTo对整体并发度的扰动更小,尤其适合消费者需要批量处理而生产者又要求低延迟的场景。
5. 实际应用场景与代码举例(JDK 8 兼容)
以下所有示例代码均基于 JDK 8,包含完整的 import 和 main 方法,可直接复制编译运行。
5.1 基础生产者-消费者模型
java
import java.util.concurrent.LinkedBlockingQueue;
public class BasicProducerConsumer {
public static void main(String[] args) {
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(5);
// 生产者线程
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
String msg = "Msg-" + i;
queue.put(msg);
System.out.println("生产: " + msg + " [队列大小: " + queue.size() + "]");
Thread.sleep(200); // 模拟生产耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Producer");
// 消费者线程
Thread consumer = new Thread(() -> {
try {
while (true) {
String msg = queue.take();
System.out.println("消费: " + msg + " [队列大小: " + queue.size() + "]");
Thread.sleep(500); // 模拟消费耗时(慢于生产,演示阻塞效果)
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Consumer");
producer.start();
consumer.start();
}
}
5.2 使用超时 offer/poll
java
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class TimeoutOfferPollExample {
public static void main(String[] args) {
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(2);
// 生产者:尝试1秒内插入,超时则放弃
Thread producer = new Thread(() -> {
String[] items = {"A", "B", "C", "D"};
for (String item : items) {
try {
boolean success = queue.offer(item, 1, TimeUnit.SECONDS);
if (success) {
System.out.println("成功插入: " + item);
} else {
System.out.println("插入失败(队列满): " + item);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
// 消费者:慢消费,500ms取一次,超时则退出
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 3; i++) {
String item = queue.poll(500, TimeUnit.MILLISECONDS);
if (item != null) {
System.out.println("消费: " + item);
Thread.sleep(1500); // 慢消费导致队列很快满
} else {
System.out.println("消费超时,退出");
break;
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
5.3 使用 drainTo 批量消费
java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
public class DrainToExample {
public static void main(String[] args) throws InterruptedException {
LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
// 快速生产100个元素
for (int i = 1; i <= 100; i++) {
queue.put(i);
}
System.out.println("初始队列大小: " + queue.size());
// 批量消费,每次最多取30个
List<Integer> batch = new ArrayList<>();
int totalDrained = 0;
while (!queue.isEmpty()) {
int drained = queue.drainTo(batch, 30);
totalDrained += drained;
System.out.printf("本批取出 %d 个元素,内容: %s%n", drained, batch);
batch.clear();
}
System.out.println("总共取出: " + totalDrained);
}
}
5.4 结合线程池(任务队列与拒绝策略)
java
import java.util.concurrent.*;
public class ThreadPoolWithLBQ {
public static void main(String[] args) {
// 核心线程1,最大线程2,队列容量10
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 2, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略:抛异常
);
// 提交20个任务,超出 maxPoolSize + queueCapacity = 2 + 10 = 12
for (int i = 1; i <= 20; i++) {
final int taskId = i;
try {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务 " + taskId);
try {
Thread.sleep(2000); // 模拟任务执行
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
System.out.println("任务 " + taskId + " 提交成功");
} catch (RejectedExecutionException e) {
System.out.println("任务 " + taskId + " 被拒绝: " + e.getMessage());
}
}
executor.shutdown();
}
}
与 ArrayBlockingQueue 对比 :若将队列替换为 ArrayBlockingQueue,由于单锁设计,当任务提交和线程池工作线程并发访问队列时,锁竞争会更激烈。但在本例中由于任务执行耗时较长,差异不明显。高并发短任务场景下 LinkedBlockingQueue 优势更突出。
5.5 演示无界队列风险
java
import java.util.concurrent.LinkedBlockingQueue;
public class UnboundedQueueRisk {
public static void main(String[] args) throws InterruptedException {
// 无参构造,容量为 Integer.MAX_VALUE
LinkedBlockingQueue<byte[]> queue = new LinkedBlockingQueue<>();
// 生产者疯狂生产,每个元素占用1MB
Thread producer = new Thread(() -> {
int count = 0;
try {
while (true) {
queue.put(new byte[1024 * 1024]); // 1MB
count++;
if (count % 100 == 0) {
System.out.println("已生产: " + count + " MB");
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
// 消费者几乎不消费(注释掉或极慢)
// 观察内存使用情况(可通过 jvisualvm 或 top 查看)
Thread.sleep(10000);
System.out.println("队列当前大小: " + queue.size());
// 最终可能抛出 OutOfMemoryError
}
}
运行此代码前请确保 JVM 堆内存设置较小(如 -Xmx256m),以便更快观察到 OOM。
6. 吞吐量与性能分析
6.1 双锁分离设计对吞吐量的提升
在 ArrayBlockingQueue 中,put 和 take 操作竞争同一把锁。当并发度升高时,锁竞争成为瓶颈,CPU 大量时间消耗在上下文切换和自旋等待上。
LinkedBlockingQueue 的双锁分离 允许生产者和消费者完全并行执行:
- 生产者线程在持有
putLock时操作链表尾部,与操作头部的消费者互不影响。 - 仅当需要更新共享变量
count时,才通过AtomicInteger的 CAS 操作进行轻量级同步。
这种设计使得 LinkedBlockingQueue 在高并发、生产消费速率相近 的场景下,吞吐量显著高于 ArrayBlockingQueue。许多基准测试表明,在 10+ 线程并发时,LinkedBlockingQueue 的吞吐量可达到 ArrayBlockingQueue 的 2~3 倍。
6.2 链表内存开销
每个元素必须封装为 Node 对象,带来额外内存开销:
- 对象头:在 64 位 JVM 开启压缩指针时约 12 字节。
- next 引用:4 字节(压缩)或 8 字节。
- 再加上
item引用(4/8 字节)。
平均每个元素额外占用约 24~32 字节。对于百万级元素,内存压力不可忽视。同时,频繁创建和丢弃 Node 对象会增加 GC 压力,可能导致 YGC 频率升高或 FGC 停顿。
6.3 无界 vs 有界
- 无界队列 (
capacity = Integer.MAX_VALUE):生产者永不阻塞(除非内存耗尽),系统失去背压(backpressure)机制,容易导致内存溢出或请求积压超时。 - 有界队列:提供天然的流量控制,但需要合理设置容量。容量过小则频繁阻塞/唤醒,过大则内存风险高。建议根据实际生产消费速率、JVM 内存和业务容忍度综合评估。
6.4 公平性
LinkedBlockingQueue 未提供公平锁参数,主要原因在于双锁设计下实现全局公平调度难度极高(需协调两把锁的等待队列)。实际应用中,双锁已经分散了竞争,线程饥饿的概率远低于单锁队列。
6.5 性能调优建议
- 合理设置容量:根据最大可容忍内存和期望的排队长度设定。可通过监控队列大小的变化趋势动态调整。
- 优先使用
offer/poll的超时版本:避免线程无限期阻塞,增强系统弹性。 - 善用
drainTo批量操作:在消费者端,批量取出元素能减少加锁次数,提升处理效率。尤其适用于日志批量写入、数据库批量插入等场景。 - 监控队列大小和内存占用:对于无界队列务必监控,防止内存泄漏。
7. 注意事项与常见陷阱
| 陷阱 | 具体原因与后果 |
|---|---|
| 无界容量可能导致内存溢出 | 默认构造器 capacity = Integer.MAX_VALUE。若消费者速度长期低于生产者,队列节点只增不减,最终堆内存耗尽抛出 OutOfMemoryError。 |
size() 方法是弱一致性 |
返回 AtomicInteger 的瞬时值。由于 count 在锁外更新,且入队/出队并发执行,size() 结果可能不反映调用后立即的准确数量。典型场景:刚刚判空后可能立即有元素入队。 |
remainingCapacity() 的瞬间性 |
返回值在下一秒可能已变化,不能用于精确的流控决策。尤其对于无界队列,返回值总是 Integer.MAX_VALUE - count,意义不大。 |
| 链表节点内部自引用帮助 GC | dequeue() 中的 h.next = h 使旧哨兵节点自引用,切断其与有效数据的联系。普通使用者无需关心,但了解此细节有助于理解为什么不会发生内存泄漏。 |
| 双锁下的信号唤醒时机必须精确 | 生产者仅在 c == 0 时唤醒消费者,消费者仅在 c == capacity 时唤醒生产者。错误实现(如每次操作都唤醒)会导致大量无效唤醒,浪费 CPU。JDK 实现正确,自定义队列时需注意。 |
| 禁止 null 元素 | put/offer 会检查元素非空,否则抛出 NullPointerException。原因:null 被用作 poll() 的特殊返回值,表示队列空。 |
| 迭代器弱一致性 | iterator() 返回的迭代器是弱一致性的,不反映创建后的修改,且不支持 remove() 操作(会抛出 UnsupportedOperationException)。 |
drainTo 不会持有整个队列的锁 |
drainTo 仅持有 takeLock,生产者仍可并发入队。但注意若目标集合操作耗时很长,仍会阻塞其他消费者。 |
8. 与 ArrayBlockingQueue 的详细对比总结
| 对比维度 | LinkedBlockingQueue | ArrayBlockingQueue |
|---|---|---|
| 数据结构 | 单向链表 | 循环数组 |
| 有界性 | 可选有界(默认 Integer.MAX_VALUE) |
必须显式指定容量,强制有界 |
| 锁数量 | 双锁(putLock / takeLock) |
单锁(lock) |
| 公平性支持 | 不支持 | 支持(fair 参数) |
| 内存分配 | 动态分配节点,每次插入新建 Node |
预分配连续数组,元素引用覆盖 |
| 内存占用 | 较高(每个元素 + Node 对象开销) | 较低(仅数组引用) |
| GC 压力 | 较大,节点频繁创建回收 | 较小,数组复用 |
| 吞吐量特征 | 高并发下更高,生产消费可并行 | 中等,高竞争时性能下降明显 |
| 典型应用场景 | 高并发生产者-消费者、线程池默认队列、对公平性无要求场景 | 固定大小缓冲区、要求公平调度、内存敏感或低 GC 场景 |
drainTo 锁持有 |
仅持有 takeLock,生产者可并发入队 |
全程持有单锁,阻塞所有操作 |
| 迭代器 | 弱一致性,不支持 remove |
弱一致性,支持 remove |
9. 总结与学习指引
核心特点回顾
- 链表结构:动态扩展,使用哨兵节点简化边界处理。
- 可选有界:无参构造为"无界",需警惕内存溢出风险。
- 双锁分离:入队锁和出队锁独立,极大提升并发吞吐量。
- 无公平参数:以吞吐量为首要设计目标,非公平调度。
- 精确唤醒:仅在边界条件跨锁唤醒,减少无效竞争。
使用建议
- 推荐场景 :
- 高并发的生产者-消费者模型。
Executors.newFixedThreadPool()等默认线程池的任务队列(需注意无界风险)。- 对内存占用和 GC 不敏感,或元素数量可控的场景。
- 不推荐场景 :
- 内存资源紧张或对 GC 停顿敏感的系统(优先考虑
ArrayBlockingQueue)。 - 需要严格公平调度的场景(考虑
ArrayBlockingQueue公平模式)。 - 需要精确控制内存占用的嵌入式环境。
- 内存资源紧张或对 GC 停顿敏感的系统(优先考虑
学习路径指引
本文是阻塞队列系列的第二篇。通过深入分析 LinkedBlockingQueue,你已经掌握了链表结构、双锁分离、条件队列协作等核心并发设计模式。下一篇文章我们将探讨零容量的同步队列------SynchronousQueue,它将揭示如何在没有内部容量的情况下实现高效的生产者-消费者握手,以及其独特的"栈"与"队列"两种公平模式实现。敬请期待!