一、引言
在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 | 正常出队 | 阻塞等待 | 是 | 是 |
核心设计精髓
-
两把锁分离:入队和出队可以并发执行,提升性能。
-
条件变量精细化控制:只在状态变化时(空→非空、满→非满)唤醒对方线程。
-
原子计数器 :
AtomicInteger保证size()的线程安全。 -
while 循环等待:防止虚假唤醒。