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 控制入队,允许入队和出队并行执行。

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

  • 单向链表 + 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() > 0 和 dequeue() 之间不需要担心队列变空?

    因为当前线程已持有 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 循环等待:防止虚假唤醒。

相关推荐
java资料站25 分钟前
十一、评估测试
开发语言·人工智能·python
白帽攻防录33 分钟前
SRC 挖洞:Roundcube 预认证 SQL 注入深度复盘,CVE-2026-48842 preg_replace 转义绕过怎么打穿邮件系统
网络·数据库·sql·网络安全·sql注入
左手の明天1 小时前
SLIP 协议封装与解析详解:原理、C 代码与实战案例
linux·c语言·开发语言·c++
shuoshuohaohao1 小时前
MyBatis 小知识点
java·spring·mybatis
海上小飞龙1 小时前
【KMP算法-下篇】同一道题:Java 库函数 2600 微秒,手写 KMP 16 微秒
java·开发语言·算法
朝朝辞暮i2 小时前
C++ 第 12 课:局部变量、作用域、变量生命周期
开发语言·c++·算法
狗凯之家源码网2 小时前
淘客口令落地页生成器 PHP 版深度评测
开发语言·php
画绛集美术2 小时前
本地化部署LLM用于美术史问答:一间美术教室的实践记录
开发语言·php
谢亮_vipxieliang2 小时前
泛型擦除、通配符与 PECS 一次讲透
java·面试
八解毒剂2 小时前
【计组】中央处理器CPU
java·开发语言·计算机组成原理