概述
在 Java 并发编程中,BlockingQueue 是生产者-消费者模式的核心组件。然而,许多开发者在使用时常常陷入无界队列 OOM 、锁竞争导致性能瓶颈 、API 误用导致死锁 等陷阱。本文将聚焦于最佳实践 ,结合 JDK 8 源码特性,为每种阻塞队列提供可直接运行的代码示例、时序图 以及详细的文字说明,帮助读者直观理解线程间的交互过程。
1. ArrayBlockingQueue 最佳实践
1.1 核心特性回顾
- 基于数组的有界阻塞队列,FIFO
- 单锁设计,入队与出队互斥
- 支持公平锁(但会显著降低吞吐量)
1.2 最佳实践:固定容量缓冲 + 背压
场景:生产者速率波动大,消费者处理能力恒定,需限制内存占用。
java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class ArrayBlockingQueueBestPractice {
private static final int QUEUE_CAPACITY = 10;
private static final BlockingQueue<String> queue = new ArrayBlockingQueue<>(QUEUE_CAPACITY);
public static void main(String[] args) {
// 启动消费者(处理能力较慢)
Thread consumer = new Thread(() -> {
while (true) {
try {
String item = queue.take();
System.out.println("消费: " + item + ", 剩余队列大小: " + queue.size());
Thread.sleep(500); // 模拟慢速消费
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}, "消费者");
consumer.setDaemon(true);
consumer.start();
// 启动生产者(快速生产)
Thread producer = new Thread(() -> {
for (int i = 0; i < 50; i++) {
try {
String item = "任务-" + i;
// 最佳实践:使用 offer 超时而非无限阻塞 put,避免永久卡死
boolean offered = queue.offer(item, 1, TimeUnit.SECONDS);
if (offered) {
System.out.println("生产: " + item);
} else {
System.err.println("队列已满,丢弃任务: " + item + "(可记录日志或触发告警)");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}, "生产者");
producer.start();
}
}
1.3 时序图:ArrayBlockingQueue 背压流程
sequenceDiagram
participant P as 生产者线程
participant Q as ArrayBlockingQueue (容量=10)
participant C as 消费者线程
Note over P,C: 初始状态:队列空
loop 快速生产任务
P->>Q: offer(任务, 1秒超时)
Q-->>P: 入队成功 (队列未满)
P->>P: 继续生产
end
Note over Q: 队列已满 (size=10)
P->>Q: offer(任务, 1秒超时)
Q-->>P: 超时返回 false
P->>P: 记录日志/丢弃任务
C->>Q: take() 获取任务
Q-->>C: 返回任务
C->>C: 处理任务 (500ms)
Note over Q: 队列出现空位
P->>Q: offer(任务, 1秒超时)
Q-->>P: 入队成功
说明:
- 初始阶段 :队列为空,生产者开始快速调用
offer(任务, 1秒超时)。由于队列有剩余容量,任务直接入队成功。 - 背压触发 :当队列被填满(
size=10)时,生产者再次尝试offer。队列检测到已满,让当前线程等待最多 1 秒。若 1 秒内仍无空间,offer返回false。此时生产者主动丢弃任务并记录告警,而非无限阻塞,这是防止生产者线程永久卡死的关键措施。 - 消费者缓解压力 :消费者线程以固定 500ms 间隔调用
take()取出任务。每次取出后,队列出现一个空位。 - 恢复生产 :当生产者下一次调用
offer时,队列已有空位,任务立即入队成功。这种机制实现了流量削峰填谷。
1.4 避坑指南
| 陷阱 | 错误做法 | 最佳实践 |
|---|---|---|
| 公平锁性能陷阱 | new ArrayBlockingQueue(100, true) |
除非有严格的线程饥饿问题,永远使用非公平锁(默认) |
| 永久阻塞 | queue.put(item) 且消费者可能崩溃 |
使用 offer(item, timeout, unit) 并处理失败情况 |
| 迭代器弱一致性 | 依赖 iterator() 做业务判断 |
迭代器是弱一致的,仅用于监控,不可用于同步控制 |
2. LinkedBlockingQueue 最佳实践
2.1 核心特性回顾
- 单向链表,双锁(putLock 与 takeLock),入队出队可并行
- 默认无界(
Integer.MAX_VALUE),也可指定容量变为有界
2.2 最佳实践:显式设置容量 + 批量消费
java
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicLong;
public class LinkedBlockingQueueBestPractice {
// 最佳实践:永远显式指定容量,避免无界队列导致 OOM
private static final int MAX_CAPACITY = 1000;
private static final BlockingQueue<Task> queue = new LinkedBlockingQueue<>(MAX_CAPACITY);
private static final AtomicLong producedCount = new AtomicLong(0);
static class Task {
private final long id;
private final byte[] payload = new byte[1024]; // 模拟1KB负载
public Task(long id) { this.id = id; }
public void process() { /* 模拟业务处理 */ }
}
public static void main(String[] args) {
// 启动批量消费者
for (int i = 0; i < 3; i++) {
new Thread(() -> {
java.util.List<Task> batch = new java.util.ArrayList<>(50);
while (true) {
try {
Task first = queue.take(); // 阻塞等待至少一个
batch.add(first);
queue.drainTo(batch, 49); // 批量拉取
for (Task t : batch) t.process();
System.out.println(Thread.currentThread().getName() +
" 批量处理 " + batch.size() + " 个任务");
batch.clear();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}, "批量消费者-" + i).start();
}
// 模拟大量生产者
for (int i = 0; i < 5; i++) {
new Thread(() -> {
while (true) {
try {
Task t = new Task(producedCount.incrementAndGet());
queue.put(t); // 有界队列,满时阻塞
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}).start();
}
}
}
2.3 时序图:LinkedBlockingQueue 双锁与批量消费
sequenceDiagram
participant P1 as 生产者1
participant P2 as 生产者2
participant Q as LinkedBlockingQueue (putLock + takeLock)
participant C as 批量消费者
Note over P1,P2: 双锁允许生产与消费并行
par 并行生产
P1->>Q: put(任务1) 获取 putLock
Q-->>P1: 入队成功
P2->>Q: put(任务2) 等待 putLock
and 并行消费
C->>Q: take() 获取 takeLock
Q-->>C: 返回任务 (从头部)
C->>C: 暂存到 batch
end
Note over C: 继续尝试批量拉取
C->>Q: drainTo(batch, 49) 一次性获取多个任务
Q-->>C: 返回最多49个任务
Note over C: 批量处理 batch 中的所有任务
C->>C: 遍历 batch 处理,减少锁竞争
说明:
- 双锁并行 :
LinkedBlockingQueue使用两把锁------putLock保护队尾入队,takeLock保护队头出队。时序图中,生产者1在获取putLock入队的同时,消费者可以获取takeLock进行出队,二者互不阻塞。这极大提高了并发吞吐量。 - 生产者竞争 :生产者2试图入队时,若生产者1尚未释放
putLock,则生产者2进入putLock的等待队列。 - 批量消费优化 :消费者首先调用
take()阻塞获取一个任务,然后立即调用drainTo(batch, 49)。drainTo方法会一次性将队列中最多 49 个元素转移到batch列表中,只获取一次锁 。相较于循环 50 次take(),减少了 49 次锁竞争,显著提升性能。 - 背压保护 :由于设置了
MAX_CAPACITY,当队列满时,生产者的put()会阻塞,形成天然的背压机制,防止内存溢出。
2.4 避坑指南
| 陷阱 | 错误做法 | 最佳实践 |
|---|---|---|
| 默认无界导致 OOM | new LinkedBlockingQueue() |
必须 new LinkedBlockingQueue(capacity) |
| 高频单元素消费 | 循环 take() 单个处理 |
使用 drainTo 批量拉取,减少锁竞争 |
| size() 瞬时性误用 | 依赖 size() == 0 做精确决策 |
size() 是精确但瞬时的,只能做监控 |
3. PriorityBlockingQueue 最佳实践
3.1 核心特性回顾
- 无界优先级队列,基于二叉堆
- 元素必须实现
Comparable或提供Comparator - 迭代器不保证顺序
3.2 最佳实践:高优先级任务插队
java
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class PriorityBlockingQueueBestPractice {
static class PrioritizedTask implements Comparable<PrioritizedTask> {
private final int priority; // 越小优先级越高
private final String name;
private final long createTime = System.nanoTime();
public PrioritizedTask(int priority, String name) {
this.priority = priority;
this.name = name;
}
@Override
public int compareTo(PrioritizedTask o) {
int cmp = Integer.compare(this.priority, o.priority);
return cmp != 0 ? cmp : Long.compare(this.createTime, o.createTime);
}
public void execute() {
System.out.println("执行任务: " + name + " [优先级=" + priority + "]");
}
}
public static void main(String[] args) throws InterruptedException {
BlockingQueue<PrioritizedTask> queue = new PriorityBlockingQueue<>();
// 提交不同优先级的任务
new Thread(() -> {
queue.put(new PrioritizedTask(5, "普通邮件通知"));
queue.put(new PrioritizedTask(1, "紧急订单处理"));
queue.put(new PrioritizedTask(3, "会员积分更新"));
queue.put(new PrioritizedTask(1, "支付回调验证"));
queue.put(new PrioritizedTask(4, "日志归档"));
}).start();
Thread.sleep(100);
// 消费者总是拿到最高优先级的任务
while (!queue.isEmpty()) {
PrioritizedTask task = queue.take();
task.execute();
}
}
}
3.3 时序图:PriorityBlockingQueue 优先级排序
sequenceDiagram
participant P as 生产者线程
participant Q as PriorityBlockingQueue (最小堆)
participant C as 消费者线程
P->>Q: put(优先级5: 普通邮件)
Note over Q: 堆化,堆顶=5
P->>Q: put(优先级1: 紧急订单)
Note over Q: 堆化,堆顶=1 (优先级最高)
P->>Q: put(优先级3: 积分更新)
Note over Q: 堆化,堆顶仍为1
C->>Q: take()
Q-->>C: 返回优先级1: 紧急订单
Note over Q: 重新堆化,堆顶=1 (支付回调)
C->>Q: take()
Q-->>C: 返回优先级1: 支付回调验证
Note over Q: 重新堆化,堆顶=3
C->>Q: take()
Q-->>C: 返回优先级3: 积分更新
Note over Q: 重新堆化,堆顶=4
C->>Q: take()
Q-->>C: 返回优先级4: 日志归档
Note over Q: 重新堆化,堆顶=5
C->>Q: take()
Q-->>C: 返回优先级5: 普通邮件
说明:
- 入队与堆化 :生产者每
put一个任务,队列内部都会执行**上浮(siftUp)**操作,确保堆顶始终是优先级最高的元素(priority值最小)。当第二个任务(优先级1)入队后,堆顶从 5 变为 1。 - 消费即取顶 :消费者调用
take()时,队列返回堆顶元素(当前优先级最高的任务)。取出后,队列执行**下沉(siftDown)**操作,重新选出新的堆顶。 - 同优先级 FIFO :由于
compareTo方法在优先级相同时会比较createTime,因此当两个优先级为 1 的任务先后入队时,先入队的"紧急订单处理"会先被取出,保证了同级公平性。 - 无界特性 :
PriorityBlockingQueue是无界的,put操作永远不会阻塞。但在高并发下需注意元素堆积导致的内存溢出风险。
3.4 避坑指南
| 陷阱 | 错误做法 | 最佳实践 |
|---|---|---|
| 元素可变导致堆序错乱 | 入队后修改用于排序的字段 | 排序字段必须不可变,或使用 remove + add 重新排队 |
| 迭代器不保证顺序 | 使用 iterator() 遍历获取优先级顺序 |
只有 take/poll 才能保证按优先级出队 |
| 无界导致内存泄漏 | 生产者持续高速生产且任务始终有效 | 监控队列大小,业务层进行限流 |
4. DelayQueue 最佳实践
4.1 核心特性回顾
- 元素必须实现
Delayed接口 - 内部使用
PriorityQueue按剩余延时排序 - 采用 Leader-Follower 模式减少唤醒竞争
4.2 最佳实践:订单超时自动取消
java
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class DelayQueueBestPractice {
static class OrderDelay implements Delayed {
private final String orderId;
private final long expireTimeNanos;
public OrderDelay(String orderId, long delaySeconds) {
this.orderId = orderId;
this.expireTimeNanos = System.nanoTime() + TimeUnit.SECONDS.toNanos(delaySeconds);
}
@Override
public long getDelay(TimeUnit unit) {
long diff = expireTimeNanos - System.nanoTime();
return unit.convert(diff, TimeUnit.NANOSECONDS);
}
@Override
public int compareTo(Delayed o) {
OrderDelay that = (OrderDelay) o;
return Long.compare(this.expireTimeNanos, that.expireTimeNanos);
}
public String getOrderId() { return orderId; }
}
private static final DelayQueue<OrderDelay> delayQueue = new DelayQueue<>();
public static void main(String[] args) {
// 超时处理线程
Thread timeoutHandler = new Thread(() -> {
while (true) {
try {
OrderDelay order = delayQueue.take(); // 阻塞直到有订单过期
System.out.println("订单超时,取消: " + order.getOrderId());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}, "超时处理器");
timeoutHandler.setDaemon(true);
timeoutHandler.start();
// 模拟下单,设置不同超时时间
delayQueue.put(new OrderDelay("order-1", 3));
delayQueue.put(new OrderDelay("order-2", 5));
delayQueue.put(new OrderDelay("order-3", 1));
}
}
4.3 时序图:DelayQueue 延时处理与 Leader-Follower 模式
sequenceDiagram
participant P as 生产者
participant Q as DelayQueue
participant L as Leader线程
participant F as Follower线程
P->>Q: put(order-1, 3s)
Note over Q: 堆顶=order-1 (剩余3s)
P->>Q: put(order-2, 5s)
P->>Q: put(order-3, 1s)
Note over Q: 堆顶=order-3 (剩余1s)
L->>Q: take()
Q-->>L: 堆顶未过期,成为Leader
Note over L: awaitNanos(1s) 精准休眠
F->>Q: take()
Q-->>F: 已有Leader,成为Follower
Note over F: await() 无限等待
Note over L: 1秒后自动唤醒
L->>Q: 取走 order-3
Q-->>L: 返回 order-3
L->>Q: 唤醒一个Follower (signal)
F->>Q: 被唤醒,重新检查堆顶
Note over Q: 堆顶=order-1 (剩余2s)
F->>Q: 成为新Leader,awaitNanos(2s)
说明:
- 生产者入队 :三个订单分别设置 3s、5s、1s 的超时时间。
DelayQueue内部是一个最小堆 ,按剩余过期时间排序。入队后,堆顶为order-3(最早过期)。 - Leader-Follower 模式 :
- 第一个调用
take()的线程(Leader)发现堆顶元素尚未过期(剩余 1 秒),于是调用awaitNanos(1_000_000_000)精准休眠 1 秒。 - 后续调用
take()的线程(Follower)检测到已有 Leader 存在,则调用await()无限期挂起,避免多个线程同时计时等待同一元素。
- 第一个调用
- 唤醒与交接 :1 秒后,Leader 线程被唤醒,取走
order-3。在返回前,它会调用signal()唤醒一个 Follower。 - 接力计时 :被唤醒的 Follower 检查新的堆顶
order-1(剩余 2 秒),然后自己成为 Leader ,开始精准休眠 2 秒。这种模式有效避免了惊群效应(Thundering Herd),大幅降低了 CPU 唤醒开销。
4.4 避坑指南
| 陷阱 | 错误做法 | 最佳实践 |
|---|---|---|
| 系统时间依赖 | System.currentTimeMillis() 可能被 NTP 调整 |
使用 System.nanoTime(),单调递增不受系统时间影响 |
| peek 不检查过期 | 用 peek() 判断元素是否可消费 |
必须使用 poll() 或 take(),peek() 可能返回未过期元素 |
| 大量元素积压 | 不设置过期监听,导致队列无限增长 | 监控队列大小,设置最大延迟时间 |
5. SynchronousQueue 最佳实践
5.1 核心特性回顾
- 没有内部容量,每个插入操作必须等待另一个线程的移除操作
- 公平模式:FIFO 队列;非公平模式:LIFO 栈(默认,吞吐更高)
5.2 最佳实践:CachedThreadPool 风格的任务直接交接
java
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class SynchronousQueueBestPractice {
public static void main(String[] args) {
BlockingQueue<Runnable> queue = new SynchronousQueue<>();
// 启动工作线程(类似 CachedThreadPool 的工作线程)
for (int i = 0; i < 3; i++) {
final int workerId = i;
new Thread(() -> {
while (true) {
try {
Runnable task = queue.poll(60, TimeUnit.SECONDS);
if (task == null) {
System.out.println("Worker-" + workerId + " 空闲超时,退出");
break;
}
System.out.println("Worker-" + workerId + " 执行任务");
task.run();
} catch (InterruptedException e) {
break;
}
}
}, "Worker-" + i).start();
}
// 生产者:必须等待有消费者接取
new Thread(() -> {
for (int i = 1; i <= 3; i++) {
final int taskId = i;
try {
System.out.println("提交任务 " + taskId + ",等待工作线程接取...");
queue.put(() -> System.out.println("任务 " + taskId + " 正在执行"));
System.out.println("任务 " + taskId + " 已被接取");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}, "生产者").start();
}
}
5.3 时序图:SynchronousQueue 手递手交接
sequenceDiagram
participant P as 生产者线程
participant Q as SynchronousQueue (TransferStack/Queue)
participant C1 as 工作线程1
participant C2 as 工作线程2
Note over C1,C2: 工作线程调用 poll(60s),无任务时挂起
C1->>Q: poll(60s)
Note over Q: 无数据,C1 节点入栈/队,park
C2->>Q: poll(60s)
Note over Q: C2 节点入栈/队,park
P->>Q: put(任务1)
Note over Q: 匹配等待者
Q-->>P: 交付任务给 C2 (非公平栈优先匹配栈顶)
Q->>C2: unpark,返回任务
P->>P: 任务已交接,可继续生产
C2->>C2: 执行任务
P->>Q: put(任务2)
Q-->>P: 交付任务给 C1
Q->>C1: unpark,返回任务
说明:
- 消费者预等待 :工作线程 1 和工作线程 2 先后调用
poll(60, SECONDS)。由于队列中没有数据,它们的节点(或栈帧)被挂载到SynchronousQueue的内部数据结构 上,并通过LockSupport.park()挂起。 - 生产者匹配 :生产者线程调用
put(任务1)。队列检测到有等待的消费者,直接从生产者线程的栈/寄存器中将任务引用传递给消费者,没有经过任何中间缓冲区(零拷贝语义)。 - 非公平栈的 LIFO 特性 :默认情况下,
SynchronousQueue使用非公平栈(TransferStack)。后挂起的线程(C2)位于栈顶,因此会被优先匹配唤醒。这有利于CPU 缓存热度,因为 C2 的线程上下文更可能仍在 CPU 缓存中。 - 完全解耦 :一旦任务交接完成,生产者立即返回,可继续生产下一个任务。整个过程不涉及任何锁,仅依靠 CAS 和
park/unpark,延迟极低。
5.4 避坑指南
| 陷阱 | 错误做法 | 最佳实践 |
|---|---|---|
| 误用 offer/poll 无超时 | queue.offer(task) 几乎总是返回 false |
使用 put/take 或带超时的 offer/poll |
| size() 和 peek() 无意义 | 使用 size() 判断队列状态 |
size() 永远返回 0,peek() 返回 null |
| 公平模式性能差异 | 滥用 new SynchronousQueue(true) |
非公平栈吞吐量高 30%~50%,仅在需要严格公平时才启用 |
6. LinkedTransferQueue 最佳实践
6.1 核心特性回顾
- 实现
TransferQueue接口,支持transfer和tryTransfer - 无界,基于松弛型双重队列,CAS 无锁
- 可兼具异步缓冲和同步移交语义
6.2 最佳实践:高性能事件总线(混合模式)
java
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TransferQueue;
import java.util.concurrent.TimeUnit;
public class LinkedTransferQueueBestPractice {
static class Event {
private final String type;
private final String payload;
public Event(String type, String payload) {
this.type = type;
this.payload = payload;
}
public void process() {
System.out.println("处理事件 [" + type + "]: " + payload);
}
}
private static final TransferQueue<Event> queue = new LinkedTransferQueue<>();
public static void main(String[] args) {
// 启动多个消费者
for (int i = 0; i < 2; i++) {
new Thread(() -> {
while (true) {
try {
Event event = queue.take();
event.process();
} catch (InterruptedException e) { break; }
}
}, "消费者").start();
}
// 场景1:异步提交(不等待消费者)
new Thread(() -> {
for (int i = 0; i < 3; i++) {
Event e = new Event("INFO", "异步消息 " + i);
queue.put(e);
System.out.println("异步提交: " + e.payload);
}
}).start();
// 场景2:同步移交(必须等待消费者接走)
new Thread(() -> {
try {
Event e = new Event("CRITICAL", "紧急事件");
System.out.println("准备同步移交: " + e.payload);
queue.transfer(e);
System.out.println("同步移交完成");
} catch (InterruptedException ignored) {}
}).start();
}
}
6.3 时序图:LinkedTransferQueue 双重队列的匹配与松弛
sequenceDiagram
participant P1 as 异步生产者
participant P2 as 同步生产者
participant Q as LinkedTransferQueue (双重队列)
participant C as 消费者
P1->>Q: put(异步消息1)
Note over Q: 数据节点入队 (isData=true)
P1->>P1: 立即返回,不阻塞
P1->>Q: put(异步消息2)
Note over Q: 数据节点入队
P2->>Q: transfer(紧急事件)
Note over Q: 无等待消费者,数据节点入队
Note over P2: 生产者 park,等待匹配
C->>Q: take()
Q-->>C: 匹配队首数据节点 (异步消息1)
Note over Q: 移除该节点
C->>Q: take()
Q-->>C: 匹配异步消息2
C->>Q: take()
Q-->>C: 匹配紧急事件
Q->>P2: unpark 同步生产者
P2->>P2: 同步移交完成,继续执行
说明:
- 异步缓冲模式 :生产者调用
put()将"异步消息1"和"异步消息2"入队。由于LinkedTransferQueue是无界 的,且此时无等待消费者,数据以数据节点(isData=true)的形式挂载在链表尾部。生产者不阻塞 ,立即返回。这提供了消息的短暂缓冲能力。 - 同步移交模式 :生产者调用
transfer(紧急事件)。此时队列中虽有数据节点,但没有请求节点(等待的消费者) ,因此该数据节点入队,生产者线程挂起(park),直到有消费者取走该数据。 - 消费者匹配 :消费者调用
take()时,会从链表头部开始寻找数据节点 进行匹配。它依次取走异步消息1、异步消息2。当匹配到"紧急事件"时,由于该节点关联了一个挂起的生产者线程,消费者在取走数据后会唤醒(unpark)该生产者。 - 松弛特性 :允许队列中同时存在数据和请求,允许生产者和消费者在时间上错峰 ,既不像
SynchronousQueue那么严格,也不像LinkedBlockingQueue那样只有异步模式。这种灵活性使其在高并发场景下吞吐量极佳。
6.4 避坑指南
| 陷阱 | 错误做法 | 最佳实践 |
|---|---|---|
| size() 是 O(n) | 高频调用 size() 进行监控 |
size() 会遍历链表,极耗性能,改用 getWaitingConsumerCount() |
| transfer 永久阻塞 | 在无人消费时调用 transfer |
使用 tryTransfer 或先调用 hasWaitingConsumer() 判断 |
| 无界内存风险 | 过度使用 put 且无消费者 |
结合背压策略,监控队列深度 |
7. LinkedBlockingDeque 最佳实践
7.1 核心特性回顾
- 双端阻塞队列,支持
putFirst/takeLast等 - 单锁设计,并发度较低
- 可用于工作窃取(Work Stealing)算法
7.2 最佳实践:简单工作窃取演示
java
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.BlockingDeque;
import java.util.Random;
public class LinkedBlockingDequeBestPractice {
static class WorkStealingThread extends Thread {
private final BlockingDeque<Runnable> localDeque;
private final BlockingDeque<Runnable>[] peerDeques;
private final int threadId;
public WorkStealingThread(int id, BlockingDeque<Runnable> deque,
BlockingDeque<Runnable>[] peers) {
this.threadId = id;
this.localDeque = deque;
this.peerDeques = peers;
}
@Override
public void run() {
Random rand = new Random();
while (true) {
try {
Runnable task = null;
// 1. 优先从自己队列头部取(LIFO,缓存友好)
task = localDeque.pollFirst();
// 2. 如果自己队列空,尝试从其他队列尾部窃取(FIFO,大任务)
if (task == null) {
for (int i = 0; i < peerDeques.length; i++) {
int victim = (threadId + i + 1) % peerDeques.length;
task = peerDeques[victim].pollLast();
if (task != null) {
System.out.println("线程-" + threadId + " 从线程-" +
victim + " 窃取了一个任务");
break;
}
}
}
// 3. 仍然没有任务,阻塞等待
if (task == null) {
task = localDeque.takeFirst();
}
task.run();
} catch (InterruptedException e) {
break;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
final int THREADS = 3;
BlockingDeque<Runnable>[] deques = new BlockingDeque[THREADS];
WorkStealingThread[] threads = new WorkStealingThread[THREADS];
for (int i = 0; i < THREADS; i++) {
deques[i] = new LinkedBlockingDeque<>(100);
}
for (int i = 0; i < THREADS; i++) {
threads[i] = new WorkStealingThread(i, deques[i], deques);
threads[i].start();
}
// 模拟不均匀任务分配:线程0有大量任务
for (int i = 0; i < 10; i++) {
final int taskId = i;
deques[0].putLast(() -> {
System.out.println(Thread.currentThread().getName() +
" 执行任务 " + taskId);
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
});
}
Thread.sleep(5000);
for (WorkStealingThread t : threads) t.interrupt();
}
}
7.3 时序图:LinkedBlockingDeque 工作窃取流程
sequenceDiagram
participant T0 as 线程0 (拥有者)
participant D0 as Deque0
participant T1 as 线程1 (窃取者)
participant D1 as Deque1
participant T2 as 线程2 (窃取者)
participant D2 as Deque2
Note over T0: 本地产生大量任务
loop 生产任务
T0->>D0: putLast(任务) (尾部入队)
end
Note over T0: 处理自己的任务 (从头部取)
T0->>D0: takeFirst() 获取任务
D0-->>T0: 返回任务 (LIFO)
T0->>T0: 执行任务
Note over T1: 自己的队列为空,尝试窃取
T1->>D1: pollFirst() 返回 null
T1->>D0: pollLast() 从 T0 队列尾部窃取
D0-->>T1: 返回任务 (FIFO, 大粒度)
T1->>T1: 执行窃取的任务
Note over T2: 同样尝试窃取
T2->>D2: pollFirst() 返回 null
T2->>D0: pollLast() 从 T0 队列尾部窃取
D0-->>T2: 返回任务
T2->>T2: 执行窃取的任务
Note over T0,T2: 所有线程忙碌,负载均衡
说明:
- 任务分配不均 :线程 0 被分配了 10 个任务,全部通过
putLast放入自己的双端队列Deque0的尾部。 - 本地消费(LIFO) :线程 0 处理自己的任务时,调用
takeFirst()从头部取出。这是**后进先出(LIFO)**策略,有利于保持 CPU 缓存热度,也符合深度优先递归的工作模式。 - 窃取机制(FIFO) :线程 1 和线程 2 的本地队列为空,它们遍历其他线程的队列,调用
pollLast()从尾部 窃取任务。这是先进先出(FIFO)策略,窃取到的是最早入队的、粒度可能较大的任务。这种头部消费 + 尾部窃取 的设计,将竞争点限制在队列尾部 ,极大地减少了线程间的锁冲突(虽然LinkedBlockingDeque仍是单锁,但窃取算法本身的设计思想如此)。 - 单锁瓶颈提示 :在真实的高并发窃取场景(如 ForkJoinPool)中,
LinkedBlockingDeque的单锁会成为瓶颈,因此 JDK 专门实现了无锁的WorkQueue。此示例仅用于演示双端队列的基本用法。
7.4 避坑指南
| 陷阱 | 错误做法 | 最佳实践 |
|---|---|---|
| 单锁瓶颈 | 高并发下使用 LinkedBlockingDeque 替代 LinkedBlockingQueue |
若无需双端操作,用 LinkedBlockingQueue 获得双锁并发度 |
| remove(Object) 遍历开销 | 频繁调用 remove 删除特定元素 |
设计业务逻辑避免删除中间元素 |
| 容量未指定 | new LinkedBlockingDeque() 默认无界 |
生产环境必须指定容量,防止 OOM |
8. 综合最佳实践总结
8.1 通用原则
| 原则 | 说明 |
|---|---|
| 永远指定容量 | 对于 LinkedBlockingQueue、LinkedBlockingDeque 必须显式设置上限 |
| 使用批量操作 | drainTo 可大幅减少锁竞争,提升吞吐量 |
| 正确处理中断 | put/take 抛出 InterruptedException 时,应恢复中断状态并退出循环 |
| 监控队列深度 | 将队列 size 作为指标暴露给监控系统,用于容量规划和背压预警 |
| 禁止 null 元素 | 所有阻塞队列都不接受 null,会立即抛出 NPE |
8.2 反模式速查表
| 反模式 | 后果 | 正确做法 |
|---|---|---|
new LinkedBlockingQueue() |
流量突发时 OOM | new LinkedBlockingQueue(capacity) |
在 DelayQueue 中放入百万级任务 |
堆排序性能下降,唤醒开销大 | 使用 ScheduledThreadPoolExecutor 或时间轮 |
单生产者多消费者用 ArrayBlockingQueue |
单锁成为瓶颈 | 使用 LinkedBlockingQueue 或 LinkedTransferQueue |
依赖 SynchronousQueue.size() 做判断 |
永远为 0,逻辑错误 | 使用 hasWaitingConsumer() 等方法 |
PriorityBlockingQueue 元素入队后修改比较字段 |
堆序错乱,出队顺序不可预测 | 使用不可变对象或 remove 后重新 add |
8.3 选型速查表
| 需求场景 | 推荐队列 | 容量策略 |
|---|---|---|
| 固定大小内存缓冲区 | ArrayBlockingQueue |
必须指定容量 |
| 高吞吐生产者-消费者(无界或大容量) | LinkedBlockingQueue |
必须指定容量上限 |
| 任务需要按优先级执行 | PriorityBlockingQueue |
无界,需业务限流 |
| 订单超时、延迟任务 | DelayQueue |
监控积压,设置最大延迟 |
| 线程池直接交接任务(CachedPool) | SynchronousQueue |
容量为 0 |
| 需要同步移交所有权(零拷贝) | LinkedTransferQueue |
无界,注意背压 |
| 工作窃取、双端消费 | LinkedBlockingDeque |
必须指定容量 |
9. 附录:可独立运行的测试类
java
import java.util.concurrent.*;
public class BlockingQueueDemo {
public static void main(String[] args) throws Exception {
testArrayBlockingQueue();
testLinkedBlockingQueue();
testPriorityBlockingQueue();
testDelayQueue();
testSynchronousQueue();
testLinkedTransferQueue();
testLinkedBlockingDeque();
}
private static void testArrayBlockingQueue() throws InterruptedException {
System.out.println("=== ArrayBlockingQueue 测试 ===");
BlockingQueue<String> q = new ArrayBlockingQueue<>(2);
q.put("A");
q.put("B");
System.out.println("队列满时 size = " + q.size());
System.out.println("取出: " + q.take());
q.put("C");
System.out.println("取出: " + q.take() + ", " + q.take());
}
private static void testLinkedBlockingQueue() throws InterruptedException {
System.out.println("\n=== LinkedBlockingQueue 测试 ===");
BlockingQueue<String> q = new LinkedBlockingQueue<>(2);
q.offer("A");
q.offer("B");
boolean success = q.offer("C", 10, TimeUnit.MILLISECONDS);
System.out.println("超时插入结果: " + success);
}
private static void testPriorityBlockingQueue() throws InterruptedException {
System.out.println("\n=== PriorityBlockingQueue 测试 ===");
BlockingQueue<Integer> q = new PriorityBlockingQueue<>();
q.put(5);
q.put(1);
q.put(3);
System.out.println("出队顺序: " + q.take() + ", " + q.take() + ", " + q.take());
}
private static void testDelayQueue() throws InterruptedException {
System.out.println("\n=== DelayQueue 测试 ===");
DelayQueue<DelayedElement> q = new DelayQueue<>();
q.put(new DelayedElement("任务1", 2000));
q.put(new DelayedElement("任务2", 1000));
System.out.println("先到期的是: " + q.take().name);
System.out.println("后到期的是: " + q.take().name);
}
static class DelayedElement implements Delayed {
String name;
long expireTime;
DelayedElement(String name, long delayMs) {
this.name = name;
this.expireTime = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(delayMs);
}
public long getDelay(TimeUnit unit) {
return unit.convert(expireTime - System.nanoTime(), TimeUnit.NANOSECONDS);
}
public int compareTo(Delayed o) {
return Long.compare(this.expireTime, ((DelayedElement)o).expireTime);
}
}
private static void testSynchronousQueue() {
System.out.println("\n=== SynchronousQueue 测试 ===");
SynchronousQueue<String> q = new SynchronousQueue<>();
new Thread(() -> {
try {
System.out.println("生产者准备放入...");
q.put("数据");
System.out.println("生产者放入成功");
} catch (InterruptedException e) {}
}).start();
new Thread(() -> {
try {
Thread.sleep(100);
System.out.println("消费者取出: " + q.take());
} catch (InterruptedException e) {}
}).start();
try { Thread.sleep(500); } catch (InterruptedException e) {}
}
private static void testLinkedTransferQueue() throws InterruptedException {
System.out.println("\n=== LinkedTransferQueue 测试 ===");
TransferQueue<String> q = new LinkedTransferQueue<>();
q.put("异步消息");
boolean transferred = q.tryTransfer("尝试移交", 100, TimeUnit.MILLISECONDS);
System.out.println("tryTransfer 结果: " + transferred);
System.out.println("队列中元素: " + q);
}
private static void testLinkedBlockingDeque() throws InterruptedException {
System.out.println("\n=== LinkedBlockingDeque 测试 ===");
BlockingDeque<String> q = new LinkedBlockingDeque<>(3);
q.putFirst("头1");
q.putLast("尾1");
q.putFirst("头2");
System.out.println("队列内容: " + q);
System.out.println("pollFirst: " + q.pollFirst());
System.out.println("pollLast: " + q.pollLast());
}
}
结语
掌握阻塞队列的最佳实践,意味着你能够:
- 避免生产事故:通过显式容量设置防止 OOM
- 提升系统吞吐:利用批量操作和合适的锁机制
- 实现精确控制:根据业务选择合适的移交语义(异步、同步、延迟)