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 循环等待:防止虚假唤醒。

相关推荐
TELL52111 分钟前
selenium webdriver 第二次初始化的异常
开发语言·python
动词ing39 分钟前
【C语言】自定义函数+指针入门
c语言·开发语言·算法
重生之后端学习1 小时前
283. 移动零[简单]✅
开发语言·数据结构·算法·leetcode·职场和发展
吠品1 小时前
Wine 在 Linux 上运行 Windows 软件完整指南
java·linux·服务器
YXXY3131 小时前
网络基础2(三)
网络
动词ing2 小时前
【C语言】结构体+文件基础
c语言·开发语言·数据结构
caimouse2 小时前
ReactOS 窗口系统分析(7):分层窗口与绘制辅助 — layered.c + draw.c
c语言·开发语言
亚历克斯神2 小时前
智能搜索系统的升级复盘——从 Elasticsearch 到混合检索的检索质量提升
java·spring·微服务
m24tyy2 小时前
常见空间测绘搜索引擎介绍和使用以及子域名的收集
网络·测试工具·搜索引擎
丰锋ff2 小时前
基于 Qt 的智慧社区物业管理系统
开发语言·qt