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

相关推荐
杨浦老苏1 小时前
网络连接实时可视化利器TapMap
网络·docker·可视化·监控·群晖
不会C语言的男孩1 小时前
C++ Primer Plus 第10章:对象和类
开发语言·c++
不会C语言的男孩2 小时前
C++ Primer Plus 第11章:使用类
开发语言·c++
未若君雅裁2 小时前
算法复杂度与数据结构:Java 集合篇的第一块基石
java·数据结构·算法
致Great2 小时前
Claude Code 上线 Dynamic Workflows:一句话调度 1000 个子智能体并行干活
java·linux·服务器
yujunl2 小时前
NetCore常用的中间件说明
开发语言
一个做软件开发的牛马2 小时前
Java 常用类:String不可变、新时间API与包装类陷阱
java·后端
m0_738120722 小时前
渗透测试基础——黑盒测试下的Web漏洞挖掘与利用解析(一)
服务器·前端·网络·安全·php
yurenpai(27届找实习中)2 小时前
redis_点评(25.附件店铺—把数据库里的店铺按【类型分组】,批量导入Redis 的 GEO 地理位置结构)
java·redis·缓存