JAVA数据结构与算法 - 基础:BlockingQueue

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() 不支持 不支持
  • 抛异常 :操作无法立即完成时抛出 IllegalStateExceptionNoSuchElementException
  • 返回特殊值:操作无法完成时返回 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();
    }
}

enqueuedequeue 都是 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() 会唤醒等待线程重新检查队首。

九、最佳实践总结

  1. 生产者-消费者首选 BlockingQueue:不用手写 wait/notify,代码更简洁健壮
  2. 有界队列优于无界队列:避免内存溢出,有界队列的生产者阻塞是最好的反压(backpressure)机制
  3. 超时版本的 offer/poll 更安全:避免线程永久阻塞,设置合理的超时时间
  4. 线程池队列选择要慎重:无界队列可能导致 OOM,SynchronousQueue 可能无限创建线程
  5. 注意 InterruptedException 处理:阻塞操作(put/take)被中断时抛出 InterruptedException,务必正确恢复中断状态或终止线程
  6. 避免在锁内执行耗时操作:BlockingQueue 内部已经加锁,不要在 put/take 的调用上下文中持有其他锁,防止死锁
相关推荐
哪吒编程2 小时前
GPT 5.5 Thinking深度思考了十几分钟,给我挖了一个排查一周的并发大坑
java
likerhood2 小时前
设计模式 · 享元模式(Flyweight Pattern)java
java·设计模式·享元模式
Royzst2 小时前
图书管理案例
java·开发语言
8K超高清2 小时前
CCBN展会多图回顾
人工智能·算法·fpga开发·接口隔离原则·智能硬件
带刺的坐椅2 小时前
SolonCode v2026.5.21 发布,Web 能看项目,IM 能找队友
java·ai编程·数字员工·soloncode·终端智能体
dunky2 小时前
副本机制与 ISR 设计:为什么 Kafka 这么快又这么可靠
java
夕除2 小时前
spring boot 9
java·mysql·spring
执明wa2 小时前
从 T 到协变逆变
java·开发语言·数据结构
XiYang-DING2 小时前
【Java EE】 TCP—异常情况处理
java·tcp/ip·java-ee