【阻塞队列的等待唤醒机制】

阻塞队列的等待唤醒机制

  • Java阻塞队列的等待唤醒机制详解及自定义实现
    • [1. 阻塞队列的基本概念](#1. 阻塞队列的基本概念)
    • [2. ArrayBlockingQueue源码剖析(等待唤醒机制)](#2. ArrayBlockingQueue源码剖析(等待唤醒机制))
    • [3. 等待唤醒机制的两种实现方式](#3. 等待唤醒机制的两种实现方式)
    • [4. 自定义一个简单阻塞队列](#4. 自定义一个简单阻塞队列)
    • [5. 常见面试延伸问题](#5. 常见面试延伸问题)
  • 总结

Java阻塞队列的等待唤醒机制详解及自定义实现

今天来分享一个经典的多线程话题:阻塞队列(BlockingQueue)的等待唤醒机制

  • 在多线程编程中,生产者-消费者模式是最常见的场景之一。而阻塞队列正是解决这个模式的核心工具。它能在队列满时阻塞生产者、在队列空时阻塞消费者,从而实现线程间安全高效的协作。

本文将从以下几个方面进行讲解:

  1. 阻塞队列的基本概念和作用
  2. Java并发包中阻塞队列的实现原理(以ArrayBlockingQueue为例)
  3. 等待唤醒机制的核心:wait()、notify() 与 Lock + Condition
  4. 手把手自定义一个简单阻塞队列
  5. 常见面试题延伸

1. 阻塞队列的基本概念

阻塞队列(BlockingQueue)是java.util.concurrent包下的接口,它的典型实现有:

  • ArrayBlockingQueue:基于数组的有界阻塞队列
  • LinkedBlockingQueue:基于链表的有界/无界阻塞队列
  • PriorityBlockingQueue:支持优先级的无界阻塞队列
  • SynchronousQueue:不存储元素的阻塞队列(容量为0)

阻塞队列的核心操作:

  • put(e) :向队列尾部添加元素,如果队列已满,则阻塞当前线程,直到有空间
  • take() :从队列头部移除元素,如果队列为空,则阻塞当前线程,直到有元素

这种"阻塞"行为,正是通过等待唤醒机制实现的。

2. ArrayBlockingQueue源码剖析(等待唤醒机制)

我们以最常用的ArrayBlockingQueue为例,看看它是如何实现阻塞的。

核心字段

javascript 复制代码
final ReentrantLock lock;                 // 单一锁控制所有操作
private final Condition notEmpty;         // 消费者等待条件(队列不空)
private final Condition notFull;          // 生产者等待条件(队列不满)
private final Object[] items;             // 底层数组
int count;                                // 当前元素数量
put操作(入队)
Java
public void put(E e) throws InterruptedException {
    Objects.requireNonNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length) {    // 队列满时
            notFull.await();               // 阻塞生产者,释放锁
        }
        enqueue(e);                       // 实际入队
        if (++count == items.length)      // 如果入队后变满,无需唤醒生产者
            return;
        notEmpty.signal();                // 唤醒一个等待的消费者
    } finally {
        lock.unlock();
    }
}
take操作(出队)
Java
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0) {              // 队列空时
            notEmpty.await();             // 阻塞消费者,释放锁
        }
        E item = dequeue();               // 实际出队
        if (--count == 0)                 // 如果出队后变空,无需唤醒消费者
            return item;
        notFull.signal();                 // 唤醒一个等待的生产者
    } finally {
        lock.unlock();
    }
}

关键点总结:

使用单一ReentrantLock保证线程安全

通过两个Condition(notEmpty、notFull)精确控制唤醒:

生产者只唤醒消费者(notEmpty.signal())

消费者只唤醒生产者(notFull.signal())

await() 会释放锁,让其他线程有机会操作队列

signal() 只唤醒一个线程,避免惊群效应

这比使用synchronized + wait/notify更高效、更灵活(可以有多个Condition)。

3. 等待唤醒机制的两种实现方式

方式一:synchronized + wait/notify(传统方式)

wait():当前线程释放锁并进入WAITING状态

notify():随机唤醒一个等待线程

notifyAll():唤醒所有等待线程(容易造成惊群)

方式二:Lock + Condition(推荐方式)

condition.await():类似wait()

condition.signal():类似notify()

condition.signalAll():类似notifyAll()

优势:一个Lock可以创建多个Condition,实现精确唤醒

阻塞队列普遍采用第二种方式。

4. 自定义一个简单阻塞队列

我们来手写一个基于数组的有界阻塞队列,使用synchronized + wait/notify实现(便于理解):

javascript 复制代码
Java
import java.util.concurrent.atomic.AtomicInteger;

public class MyBlockingQueue<E> {
    private final Object[] items;
    private int takeIndex;      // 消费指针
    private int putIndex;       // 生产指针
    private final AtomicInteger count = new AtomicInteger(0);
    private final int capacity;

    public MyBlockingQueue(int capacity) {
        this.capacity = capacity;
        this.items = new Object[capacity];
    }

    public synchronized void put(E e) throws InterruptedException {
        while (count.get() == capacity) {   // 队列满
            wait();                         // 释放锁,阻塞生产者
        }
        items[putIndex] = e;
        if (++putIndex == capacity) {
            putIndex = 0;
        }
        count.incrementAndGet();
        notifyAll();                        // 唤醒消费者(这里用notifyAll避免遗漏)
    }

    public synchronized E take() throws InterruptedException {
        while (count.get() == 0) {          // 队列空
            wait();                         // 释放锁,阻塞消费者
        }
        @SuppressWarnings("unchecked")
        E item = (E) items[takeIndex];
        items[takeIndex] = null;            // help GC
        if (++takeIndex == capacity) {
            takeIndex = 0;
        }
        count.decrementAndGet();
        notifyAll();                        // 唤醒生产者
        return item;
    }
}

测试代码:

javascript 复制代码
Java
public class Test {
    public static void main(String[] args) {
        MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(5);

        // 生产者
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    queue.put(i);
                    System.out.println("生产: " + i);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }).start();

        // 消费者
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    Integer val = queue.take();
                    System.out.println("消费: " + val);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }).start();
    }
}

运行结果会看到生产者和消费者交替进行,当队列满/空时自动阻塞。

注意:实际生产中推荐使用Lock + Condition,因为notifyAll()可能导致所有线程都被唤醒后再竞争锁,性能较低。

5. 常见面试延伸问题

为什么不直接用notify而是用notifyAll?

在环形队列中,生产者和消费者都在同一个锁上等待,使用notify()可能唤醒同类型线程,导致继续等待(死等)。notifyAll()更安全。

ArrayBlockingQueue为什么用单一锁而不是读写分离?

为了实现强一致性(入队立即对出队可见),且数组实现下读写锁分离收益不大。

LinkedBlockingQueue为什么用两把锁?

链表头尾操作相对独立,使用takeLock和putLock分离,提高并发度。

总结

  • 阻塞队列的等待唤醒机制本质是:在临界区判断条件不满足时,释放锁并等待;条件满足后唤醒等待线程。

  • Java并发包通过ReentrantLock + Condition实现了高效精确的阻塞队列,是我们日常开发中线程安全协作的利器。

希望本文对你理解阻塞队列的底层原理有所帮助!如果有问题欢迎留言讨论~

点赞 + 收藏 + 关注,三连走一走~

相关推荐
骚戴4 小时前
深入解析:Gemini 3.0 Pro 的 SSE 流式响应与跨区域延迟优化实践
java·人工智能·python·大模型·llm
毕设源码-朱学姐4 小时前
【开题答辩全过程】以 基于Java技术的羽毛球积分赛管理系统的设计与实现 为例,包含答辩的问题和答案
java·开发语言
2501_941982054 小时前
Go 进阶:发送文件/图片消息的流程与实现
开发语言·后端·golang
疾风sxp4 小时前
nl2sql技术实现自动sql生成之Spring AI Alibaba Nl2sql
java·人工智能
BullSmall4 小时前
Tomcat11证书配置全指南
java·运维·tomcat
永不停歇的蜗牛4 小时前
K8S之创建cm指令create和 apply的区别
java·容器·kubernetes
爱学习的小可爱卢5 小时前
JavaEE进阶——SpringBoot统一功能处理全解析
java·spring boot·后端·java-ee
CoderCodingNo5 小时前
【GESP】C++一级真题 luogu-B4410 [GESP202509 一级] 金字塔
开发语言·c++