Java并发编程 :深入剖析LinkedBlockingQueue

一、引言

在Java并发编程中,LinkedBlockingQueue 是一个非常重要的阻塞队列实现。它广泛应用于线程池(如 ThreadPoolExecutor)、生产者-消费者模型等场景。本文将带你从源码角度,深入理解它的数据结构、锁机制、条件变量以及核心方法的实现原理。

二、整体架构与类图分析

LinkedBlockingQueue 继承自 AbstractQueue,实现了 BlockingQueue 接口。其内部通过单向链表存储元素,并可选地设置容量上限。

核心属性

java 复制代码
static class Node<E> {
    E item;
    Node<E> next;
    Node(E x) { item = x; }
}

private final int capacity;                      // 队列容量
private final AtomicInteger count = new AtomicInteger(); // 当前元素个数

private transient Node<E> head;                  // 头节点(dummy)
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();     // 非满条件

设计亮点

  • 两把锁设计takeLock 控制出队,putLock 控制入队,允许入队和出队并行执行

  • 两个条件变量notEmptynotFull 分别管理消费者和生产者线程的等待与唤醒。

  • 单向链表 + dummy head:头节点不存储实际元素,简化边界处理。

三、入队操作源码分析

1. offer(e) ------ 非阻塞入队

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);
    final ReentrantLock putLock = this.putLock;
    putLock.lock();                // ② 获取入队锁
    try {
        if (count.get() < capacity) { // ③ 再次检查(双重检查)
            enqueue(node);           // ④ 入队
            c = count.getAndIncrement(); // ⑤ 元素个数+1
            if (c + 1 < capacity)    // ⑥ 仍有空位,唤醒下一个生产者
                notFull.signal();
        }
    } finally {
        putLock.unlock();            // ⑦ 释放锁
    }
    if (c == 0)                      // ⑧ 从0→1,说明队列非空,唤醒消费者
        signalNotEmpty();
    return c >= 0;
}

private void enqueue(Node<E> node) {
    last = last.next = node;         // 尾部追加
}
🔥 关键点解读
  • 双重检查count.get() == capacity 判断后,到获取锁期间可能被其他线程填满,因此锁内需再次判断。

  • 唤醒生产者的条件c + 1 < capacity 表示入队后仍未满,可提前唤醒下一个生产者,提升吞吐量。

  • 唤醒消费者条件c == 0 表示从空变为非空,此时需要唤醒等待的消费者。

2. put(e) ------ 阻塞入队

java 复制代码
public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    int c = -1;
    Node<E> node = new Node<>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();          // 可中断加锁
    try {
        while (count.get() == capacity) { // ① 满则等待
            notFull.await();
        }
        enqueue(node);
        c = count.getAndIncrement();
        if (c + 1 < capacity)
            notFull.signal();             // ② 仍有余位,唤醒其他生产者
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();                 // ③ 从空变非空,唤醒消费者
}
🔥 与 offer 的区别
  • 阻塞行为 :队列满时,put 会调用 notFull.await() 让出锁并挂起当前线程。

  • 可中断lockInterruptibly() 使得线程在等待锁时可以被中断。

  • while 循环:防止虚假唤醒,确保条件满足后才继续。

四、出队操作源码分析

1. poll() ------ 非阻塞出队

java 复制代码
public E poll() {
    final AtomicInteger count = this.count;
    if (count.get() == 0)
        return null;
    E x = null;
    int c = -1;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        if (count.get() > 0) {        // ① 再次检查非空
            x = dequeue();             // ② 出队
            c = count.getAndDecrement(); // ③ 元素个数-1
            if (c > 1)                 // ④ 出队后仍非空,唤醒其他消费者
                notEmpty.signal();
        }
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)                 // ⑤ 从满→非满,唤醒生产者
        signalNotFull();
    return x;
}

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;
}
🔥 线程安全分析
  • 为什么 count.get() > 0dequeue() 之间不需要担心队列变空?

    因为当前线程已持有 takeLock,其他出队操作无法执行。入队操作虽然可以并行执行,但它们只会增加 count,不会减少,因此安全。

2. 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 {
        while (count.get() == 0) {   // ① 空则等待
            notEmpty.await();
        }
        x = dequeue();
        c = count.getAndDecrement();
        if (c > 1)                   // ② 仍有元素,唤醒其他消费者
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)               // ③ 从满变非满,唤醒生产者
        signalNotFull();
    return x;
}

五、条件变量的唤醒细节

signalNotEmpty 与 signalNotFull

java 复制代码
private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
}

⚠️ 注意:调用 signal() 前必须持有对应的锁,否则会抛出 IllegalMonitorStateException

六、生产-消费者模型实战

java 复制代码
// 生产者
public void produce() throws InterruptedException {
    basket.put("An apple");   // 满则阻塞
}

// 消费者
public String consume() throws InterruptedException {
    return basket.take();     // 空则阻塞
}

示例中,两个生产者、一个消费者通过 LinkedBlockingQueue 自动完成线程同步,无需显式 wait/notify

七、总结与面试高频问题

方法 队列满时行为 队列空时行为 是否阻塞 可中断
offer 返回 false 正常入队
put 阻塞等待 正常入队
poll 正常出队 返回 null
take 正常出队 阻塞等待

核心设计精髓

  1. 两把锁分离:入队和出队可以并发执行,提升性能。

  2. 条件变量精细化控制:只在状态变化时(空→非空、满→非满)唤醒对方线程。

  3. 原子计数器AtomicInteger 保证 size() 的线程安全。

  4. while 循环等待:防止虚假唤醒。

相关推荐
nanxun8863 小时前
记一次诡异的 Docker 容器"串包"故障排查
java
用户1563068103516 小时前
Day01 | Java 基础(Java SE)
java
行者全栈架构师7 小时前
Maven dependency:tree 的 8 个高级用法
java·后端
行者全栈架构师12 小时前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端
令人头秃的代码0_012 小时前
mac(m5)平台编译openjdk
java
唐青枫1 天前
Java JDBC 实战指南:从 Connection 到事务和连接池
java
一个做软件开发的牛马1 天前
MyBatis-Plus 从零实战:完整搭建可运行 Demo,BaseMapper 零 SQL、Wrapper 条件构造、分页插件与代码生成器详解
java·后端
用户3721574261351 天前
Java 处理 PDF 图片:提取 PDF 中的图片,并压缩 PDF 图片体积
java
用户3721574261351 天前
Java 打印 Word 文档:从基础打印到高级设置
java
用户3521802454752 天前
当 Prompt 学会"热更新":Spring Boot × Nacos3 AI 实战
java·spring boot·ai编程