25、数据结构与算法 - 基础:BlockingQueue
一、概述:线程安全的生产者-消费者基石
BlockingQueue 是 Java 并发包(java.util.concurrent)中最核心的接口之一,它在普通 Queue 的基础上新增了阻塞等待 和超时等待 两种操作模式。这使得 BlockingQueue 天然适用于生产者-消费者模式------当队列已满时,生产者线程自动阻塞等待空间腾出;当队列为空时,消费者线程自动阻塞等待新元素到达。无需手动编写 wait()/notify() 的繁琐逻辑,BlockingQueue 将这些复杂的同步控制全部封装在内部
继承体系
markdown
java.util.Collection
└── java.util.Queue
└── java.util.concurrent.BlockingQueue(接口)
├── ArrayBlockingQueue ------ 有界数组阻塞队列
├── LinkedBlockingQueue ------ 可选有界链表阻塞队列
├── PriorityBlockingQueue ------ 无界优先级阻塞队列
├── DelayQueue ------ 延迟阻塞队列
├── SynchronousQueue ------ 同步移交队列(容量为 0)
└── LinkedTransferQueue ------ 链表传输队列(JDK 7+)
二、四种操作模式对比
BlockingQueue 为每个操作提供了四种处理策略,这是理解它的核心:
| 操作类型 | 立即抛异常 | 立即返回特殊值 | 无限期阻塞 | 超时阻塞 |
|---|---|---|---|---|
| 插入 | add(e) |
offer(e) |
put(e) |
offer(e, time, unit) |
| 移除 | remove() |
poll() |
take() |
poll(time, unit) |
| 查看 | element() |
peek() |
不支持 | 不支持 |
- 抛异常 :操作无法立即完成时抛出
IllegalStateException或NoSuchElementException - 返回特殊值:操作无法完成时返回 false 或 null
- 无限期阻塞:操作无法完成时,当前线程进入 WAITING 状态,直到条件满足
- 超时阻塞:在指定时间内等待,超时后返回特殊值
put(e) 和 take() 是 BlockingQueue 区别于普通 Queue 的标志性方法,它们让生产者-消费者协调从"轮询检查"变为"事件驱动",极大简化了并发编程模型。
三、五大实现类详解
3.1 ArrayBlockingQueue------有界数组队列
内部设计 :底层使用一个定长数组 Object[] items,配合一把独占锁 ReentrantLock 和两个条件变量 notEmpty(消费者等待)与 notFull(生产者等待)。所有操作共享同一把锁,这意味着同一时刻只能有一个线程执行入队或出队。
关键源码片段:
java
// put 方法的核心逻辑
public void put(E e) throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await(); // 队列满,生产者等待
enqueue(e); // 实际入队
} finally {
lock.unlock();
}
}
enqueue 和 dequeue 都是 O(1) 操作,因为 ArrayBlockingQueue 也使用了类似 ArrayDeque 的循环索引方式,元素不需要被移动。
特点:
- 容量在构造时固定,不可动态调整
- 公平性可选:
new ArrayBlockingQueue<>(capacity, true)使用公平锁,按等待时间长短分配锁 - 适合生产与消费速率相对稳定的场景
3.2 LinkedBlockingQueue------可选有界链表队列
内部设计 :基于单向链表节点,使用两把锁 分离入队和出队操作------putLock 控制尾部插入,takeLock 控制头部移除。这种设计使得生产者和消费者可以在一定程度上并行操作,吞吐量通常高于 ArrayBlockingQueue。
关键源码片段:
java
// 节点定义
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
// 入队(持 putLock)
private void enqueue(Node<E> node) {
last = last.next = node;
}
// 出队(持 takeLock)
private E dequeue() {
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
特点:
- 不指定容量时默认为
Integer.MAX_VALUE(相当于无界) - 双锁设计带来更高并发吞吐量
- 每次入队需要动态分配 Node,产生更多 GC 压力
3.3 PriorityBlockingQueue------无界优先级阻塞队列
内部设计 :与 PriorityQueue 的堆实现相同,但增加了并发控制。使用一把 ReentrantLock 和一个 notEmpty 条件变量。由于是无界队列,不存在 notFull 条件------put 永远不会阻塞。
特点:
- 元素必须可比较(实现 Comparable 或传入 Comparator)
- 出队按优先级顺序,而非 FIFO
take()在队列为空时阻塞,但put()永不阻塞- 扩容时使用 CAS 操作
allocationSpinLock进行轻量级同步
3.4 DelayQueue------延迟队列
内部设计 :基于 PriorityQueue,元素必须实现 Delayed 接口。只有延迟时间到期的元素才能被取出。内部使用一把 ReentrantLock 和一个 Condition available。
典型元素定义:
java
class DelayedTask implements Delayed {
private final long executeTime; // 到期时间戳(毫秒)
private final String taskName;
DelayedTask(String name, long delayMs) {
this.taskName = name;
this.executeTime = System.currentTimeMillis() + delayMs;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(executeTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed other) {
return Long.compare(this.executeTime, ((DelayedTask) other).executeTime);
}
}
take() 方法在队首元素的延迟时间未到时,会通过 available.awaitNanos(delay) 精确等待,这是 DelayQueue 最精妙的设计。
3.5 SynchronousQueue------零容量同步移交
SynchronousQueue 是一个不存储元素的阻塞队列。每个 put 操作必须等待另一个线程的 take,反之亦然。它更像是线程间直接传递数据的"管道",容量为 0。
SynchronousQueue 支持两种内部模式:
- 公平模式(TransferQueue):使用 FIFO 队列管理等待线程
- 非公平模式(TransferStack,默认):使用 LIFO 栈管理等待线程
典型应用 :线程池中 Executors.newCachedThreadPool() 使用 SynchronousQueue 作为任务队列,新任务到达时如果没有空闲线程则立即创建新线程。
四、五种实现的一览对比
| 特性 | ArrayBlockingQueue | LinkedBlockingQueue | PriorityBlockingQueue | DelayQueue | SynchronousQueue |
|---|---|---|---|---|---|
| 数据结构 | 定长数组 | 单向链表 | 二叉堆(数组) | 二叉堆 | 无存储结构 |
| 有界/无界 | 有界(固定) | 可选有界 | 无界 | 无界 | 容量恒为 0 |
| 锁机制 | 单锁 | 双锁(putLock + takeLock) | 单锁 | 单锁 | 自旋 + CAS |
| 排序 | FIFO | FIFO | 优先级 | 延迟时间 | 无 |
| put 阻塞 | 是 | 是(有界时) | 否 | 否 | 是 |
| take 阻塞 | 是 | 是 | 是 | 是 | 是 |
| null 元素 | 不允许 | 不允许 | 不允许 | 不允许 | 不允许 |
| 线程池中的应用 | FixedThreadPool | FixedThreadPool | newFixedThreadPool 不直接使用 | ScheduledThreadPoolExecutor | CachedThreadPool |
五、完整代码示例
示例一:经典生产者-消费者模式
java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ProducerConsumerDemo {
private static final int QUEUE_CAPACITY = 5;
private static final int TOTAL_ITEMS = 15;
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(QUEUE_CAPACITY);
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= TOTAL_ITEMS; i++) {
String item = "产品-" + i;
queue.put(item); // 队列满时自动阻塞
System.out.println("[生产者] 生产了: " + item + " 队列大小: " + queue.size());
Thread.sleep((long) (Math.random() * 300));
}
// 发送结束信号
queue.put("END");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Producer");
Thread consumer = new Thread(() -> {
try {
while (true) {
String item = queue.take(); // 队列空时自动阻塞
if ("END".equals(item)) {
System.out.println("[消费者] 收到结束信号,退出");
break;
}
System.out.println("[消费者] 消费了: " + item + " 队列大小: " + queue.size());
Thread.sleep((long) (Math.random() * 500)); // 模拟消费耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Consumer");
producer.start();
consumer.start();
producer.join();
consumer.join();
System.out.println("\n=== 所有任务完成 ===");
}
}
运行观察 :当生产者速度快于消费者时,队列逐渐填满,put() 会阻塞生产者;当消费者速度快于生产者时,take() 会阻塞消费者。这正是 BlockingQueue 的核心价值------自动协调生产消费速率。
示例二:DelayQueue 定时任务调度
java
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
class ReminderTask implements Delayed {
private final String message;
private final long triggerTime; // 触发时间戳(纳秒)
ReminderTask(String message, long delaySeconds) {
this.message = message;
this.triggerTime = System.nanoTime() + TimeUnit.SECONDS.toNanos(delaySeconds);
}
@Override
public long getDelay(TimeUnit unit) {
long remaining = triggerTime - System.nanoTime();
return unit.convert(remaining, TimeUnit.NANOSECONDS);
}
@Override
public int compareTo(Delayed other) {
ReminderTask that = (ReminderTask) other;
return Long.compare(this.triggerTime, that.triggerTime);
}
@Override
public String toString() {
return message;
}
}
public class DelayQueueScheduler {
public static void main(String[] args) throws InterruptedException {
DelayQueue<ReminderTask> scheduler = new DelayQueue<>();
// 添加不同延迟的提醒任务
scheduler.put(new ReminderTask("5秒后:检查数据库连接", 5));
scheduler.put(new ReminderTask("2秒后:刷新缓存", 2));
scheduler.put(new ReminderTask("8秒后:发送日报邮件", 8));
scheduler.put(new ReminderTask("1秒后:打印系统状态", 1));
System.out.println("=== 延迟任务调度器启动 ===");
// 消费者循环:按延迟时间顺序执行任务
while (!scheduler.isEmpty()) {
ReminderTask task = scheduler.take(); // 阻塞直到最近的任务到期
System.out.println("[执行] " + task + " (时间: " + System.currentTimeMillis() / 1000 + "s)");
}
System.out.println("=== 全部延迟任务执行完毕 ===");
}
}
设计精妙之处 :DelayQueue 内部使用 PriorityQueue 按到期时间排序,take() 方法调用 available.awaitNanos(delay) 让线程精确休眠到队首任务到期,无需轮询,CPU 零浪费。
示例三:SynchronousQueue 线程间直接传递
java
import java.util.concurrent.SynchronousQueue;
public class SynchronousQueueDemo {
public static void main(String[] args) {
SynchronousQueue<String> handoff = new SynchronousQueue<>();
// 生产者线程:每次 put 必须等待消费者 take
Thread producer = new Thread(() -> {
String[] dishes = {"鱼香肉丝", "宫保鸡丁", "麻婆豆腐", "回锅肉"};
try {
for (String dish : dishes) {
System.out.println("[厨师] 做好了: " + dish + ",等待传菜...");
handoff.put(dish); // 阻塞直到有服务员来取
System.out.println("[厨师] " + dish + " 已被取走!");
Thread.sleep(300);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Chef");
// 消费者线程:每次 take 必须等待生产者 put
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 4; i++) {
Thread.sleep(600); // 服务员走路需要时间
String dish = handoff.take(); // 阻塞直到厨师做好
System.out.println("[服务员] 取到了: " + dish + ",正在上菜...");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Waiter");
producer.start();
consumer.start();
try {
producer.join();
consumer.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("=== 餐厅打烊 ===");
}
}
关键洞察:SynchronousQueue 就像没有缓冲区的中转站------厨师做完一道菜必须等服务员来取才能做下一道,服务员必须等厨师做完才能取菜。这在需要严格控制并发度的管道模型中非常有用。
示例四:多生产者多消费者------模拟线程池任务分发
java
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
class WorkItem {
private static final AtomicInteger idGen = new AtomicInteger(0);
final int id;
final String payload;
WorkItem(String payload) {
this.id = idGen.incrementAndGet();
this.payload = payload;
}
@Override
public String toString() {
return "任务#" + id + "(" + payload + ")";
}
}
public class MultiProducerMultiConsumer {
private static final int PRODUCER_COUNT = 3;
private static final int CONSUMER_COUNT = 2;
private static final int QUEUE_SIZE = 10;
private static final int ITEMS_PER_PRODUCER = 5;
public static void main(String[] args) throws InterruptedException {
BlockingQueue<WorkItem> queue = new LinkedBlockingQueue<>(QUEUE_SIZE);
// 创建多个生产者
Thread[] producers = new Thread[PRODUCER_COUNT];
for (int i = 0; i < PRODUCER_COUNT; i++) {
final int producerId = i + 1;
producers[i] = new Thread(() -> {
try {
for (int j = 0; j < ITEMS_PER_PRODUCER; j++) {
WorkItem item = new WorkItem("P" + producerId + "-数据" + j);
boolean offered = queue.offer(item, 2, TimeUnit.SECONDS);
if (offered) {
System.out.println("[P" + producerId + "] 提交: " + item);
} else {
System.out.println("[P" + producerId + "] 提交超时: " + item);
}
Thread.sleep((long) (Math.random() * 200));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Producer-" + producerId);
}
// 创建多个消费者
Thread[] consumers = new Thread[CONSUMER_COUNT];
for (int i = 0; i < CONSUMER_COUNT; i++) {
final int consumerId = i + 1;
consumers[i] = new Thread(() -> {
try {
while (true) {
WorkItem item = queue.poll(3, TimeUnit.SECONDS);
if (item == null) {
System.out.println("[C" + consumerId + "] 超时退出");
break;
}
System.out.println("[C" + consumerId + "] 处理: " + item);
Thread.sleep((long) (Math.random() * 500));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Consumer-" + consumerId);
}
// 启动所有线程
for (Thread p : producers) p.start();
for (Thread c : consumers) c.start();
// 等待生产者完成
for (Thread p : producers) p.join();
System.out.println("\n=== 所有生产者已完成 ===");
// 等待消费者超时退出
for (Thread c : consumers) c.join();
System.out.println("=== 所有消费者已退出 ===");
}
}
设计要点:这个示例展示了超时 offer/poll 的实际用法------生产者在队列满时等待最多 2 秒后放弃,消费者在 3 秒内没有新任务时自动退出。多生产者多消费者的场景在真实系统中极为常见,如 Web 服务的请求处理、日志收集等。
六、线程安全机制详解
BlockingQueue 的线程安全基于以下核心并发原语:
- ReentrantLock:提供互斥访问,所有实现类内部至少持有一把锁
- Condition(条件变量) :
notEmpty让消费者在队列空时等待,notFull让生产者在队列满时等待 - 原子变量 :如
AtomicInteger count,在锁外也能安全读取元素计数 - CAS 操作:特定实现(如 SynchronousQueue、PriorityBlockingQueue 扩容)使用无锁 CAS 提升性能
BlockingQueue 的所有实现都不允许 null 元素,原因与 ArrayDeque 相同------null 被用作 poll/peek 失败时的哨兵值。
七、线程池中的 BlockingQueue
BlockingQueue 是 Java 线程池 ThreadPoolExecutor 的核心组件。不同线程池策略对应不同的队列选择:
| 线程池类型 | 使用队列 | 效果 |
|---|---|---|
| FixedThreadPool | LinkedBlockingQueue(默认无界) | 固定核心线程,任务无限排队 |
| CachedThreadPool | SynchronousQueue | 无排队,有任务就创建新线程 |
| SingleThreadExecutor | LinkedBlockingQueue(默认无界) | 单线程顺序执行 |
| ScheduledThreadPool | DelayedWorkQueue(类似 DelayQueue) | 定时/周期性任务调度 |
自定义线程池时可以传入任意 BlockingQueue 实现,这给了开发者极大的灵活性来控制任务排队策略。
八、常见面试题解析
Q1: ArrayBlockingQueue 和 LinkedBlockingQueue 如何选择?
ArrayBlockingQueue 使用一把锁,实现简单,内存占用可预估(定长数组 + 固定对象引用)。适合生产消费速率稳定的场景。LinkedBlockingQueue 使用两把锁(putLock 和 takeLock),入队出队可并行,吞吐量更高,但每次插入都需要分配 Node 对象,GC 压力大。高吞吐场景选 LinkedBlockingQueue,低延迟且内存紧张场景选 ArrayBlockingQueue。
Q2: SynchronousQueue 的公平和非公平模式有什么区别?
公平模式下使用 TransferQueue(FIFO),先到达的等待线程先被匹配;非公平模式使用 TransferStack(LIFO),后到达的等待线程先被匹配。非公平模式吞吐量更高(默认),公平模式避免线程饥饿。这种设计类似于锁的公平性选择。
Q3: DelayQueue 如何实现精确的延迟等待?
DelayQueue 的 take() 方法获取队首元素(最早到期的),调用 getDelay(NANOSECONDS) 获取剩余等待时间,然后用 available.awaitNanos(delay) 让线程精确休眠到到期时刻。在这期间如果有更早到期的元素被插入(通过 put),available.signal() 会唤醒等待线程重新检查队首。
九、最佳实践总结
- 生产者-消费者首选 BlockingQueue:不用手写 wait/notify,代码更简洁健壮
- 有界队列优于无界队列:避免内存溢出,有界队列的生产者阻塞是最好的反压(backpressure)机制
- 超时版本的 offer/poll 更安全:避免线程永久阻塞,设置合理的超时时间
- 线程池队列选择要慎重:无界队列可能导致 OOM,SynchronousQueue 可能无限创建线程
- 注意 InterruptedException 处理:阻塞操作(put/take)被中断时抛出 InterruptedException,务必正确恢复中断状态或终止线程
- 避免在锁内执行耗时操作:BlockingQueue 内部已经加锁,不要在 put/take 的调用上下文中持有其他锁,防止死锁