LinkedBlockingQueue
LinkedBlockingQueue 源码分析
链表实现的阻塞队列, 解决的问题和 ArrayBlockingQueue 一样(生产者-消费者), 但它把”入队”和”出队”拆成两把独立的锁: put 只碰尾巴, take 只碰头, 两边可以真正并行。
能这么拆的前提是一个 AtomicInteger count 在两把锁之间充当唯一的共享裁判。
代码块收起展开
// 基于 JDK 17。链表阻塞队列:takeLock/putLock 两锁分离,头尾各管各的,吞吐高于单锁的 ArrayBlockingQueue。
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
static class Node<E> {
E item;
Node<E> next; // 出队后会指向自己(self-link),帮 GC 断开跨代引用
Node(E x) { item = x; }
}
private final int capacity; // 容量上限。默认构造给 Integer.MAX_VALUE,"有界"名存实亡
private final AtomicInteger count = new AtomicInteger(); // 两把锁唯一共享的状态,必须原子
transient Node<E> head; // 不变式:head.item == null,是个哨兵。只有 takeLock 侧动它
private transient Node<E> last; // 不变式:last.next == null。只有 putLock 侧动它
private final ReentrantLock takeLock = new ReentrantLock(); // take/poll 用
private final Condition notEmpty = takeLock.newCondition(); // 消费者在这上面等"非空"
private final ReentrantLock putLock = new ReentrantLock(); // put/offer 用
private final Condition notFull = putLock.newCondition(); // 生产者在这上面等"非满"
// put 侧发现队列由空变非空时调用:signal 别人锁上的 Condition 必须先持有那把锁
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock(); // 跨锁唤醒的代价:put 线程偶尔要摸一下 takeLock
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
// take 侧发现队列由满变不满时调用,对称
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node; // 只动 last,不碰 head —— 两锁互不干扰的物理基础
}
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
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;
}
// remove/clear/迭代器修改这类要同时动头尾的操作,才需要两把锁全拿
void fullyLock() {
putLock.lock();
takeLock.lock();
}
void fullyUnlock() {
takeLock.unlock();
putLock.unlock();
}
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE); // 默认"无界"。Executors.newFixedThreadPool 用的就是这个构造
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null); // 哨兵节点:空队列时 head == last,两把锁也不会摸到同一个字段
}
// ...
}put 和 take 是完全对称的一对, 各自只拿自己那把锁:
代码块收起展开
// 基于 JDK 17。java.util.concurrent.LinkedBlockingQueue:put 与 take
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
final int c;
final Node<E> node = new Node<E>(e); // 节点在锁外分配,缩短持锁时间
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
while (count.get() == capacity) { // 持有 putLock 时 count 只会被 take 减小,读到"满"顶多虚惊一场
notFull.await();
}
enqueue(node);
c = count.getAndIncrement(); // c 是加之前的旧值
if (c + 1 < capacity) // 加完还没满 -> 顺手唤醒下一个生产者(级联唤醒)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0) // 旧值为 0 = 队列刚从空变非空,才需要跨锁叫醒消费者
signalNotEmpty();
}
// ...
public E take() throws InterruptedException {
final E x;
final int c;
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) // 旧值为 capacity = 刚从满变不满,才跨锁叫醒生产者
signalNotFull();
return x;
}原理串讲
先回答最核心的问题: 两把锁凭什么不打架。链表队列的入队和出队天然操作两个不同的端点, enqueue 只写 last, dequeue 只写 head, 构造函数里 last = head = new Node<E>(null) 的哨兵节点保证即使队列为空, put 改的是哨兵的 next(以 last 的身份), take 读的是 head.item(永远是 null 的哨兵), 结构上没有任何字段会被两侧同时写。
唯一逃不掉的共享状态是元素个数: put 要用它判满, take 要用它判空, 而它同时被两把锁下的代码修改, 谁的锁都罩不住它, 所以必须是 AtomicInteger, 用 getAndIncrement/getAndDecrement 保证原子并顺带拿到旧值。
对比之下 ArrayBlockingQueue 的 takeIndex/putIndex/count 全是普通 int, 因为它只有一把锁, 所有状态都在同一把锁的保护下。
数组做不了双锁还有个更根本的原因: 环形数组里 putIndex 追上 takeIndex 时两个”端点”就是同一个槽位, 头尾在物理上分不开。
再看 while (count.get() == capacity) 这个等待条件, 它读的是没有被当前锁保护的变量, 看起来像数据竞争, 其实是精心安排过的单向误差: 持有 putLock 期间其他生产者全被挡在外面, count 只可能被消费者减小。
所以这个读要么准确, 要么读到一个”偏大”的过期值——把不满误判成满, 大不了在 notFull 上多睡一轮, 而队列真的从满变不满时 take 一定会 signalNotFull, 不会睡死; 反过来”把满误判成不满”这种会导致超容量的错误方向, 被锁排除了。
源码里那段 “count is used in wait guard even though it is not protected by lock” 的注释讲的正是这件事。
然后是唤醒设计, 这是双锁版本最精巧的地方。put 成功后做两个判断: c + 1 < capacity 时 signal 自己锁上的 notFull, c == 0 时才跨锁 signalNotEmpty。
前者是级联唤醒(cascading signal): 一次 signal 只叫醒一个等待者, 那如果十个生产者都在 notFull 上睡, 消费者只在”从满变不满”那一刻 signal 一次, 剩下九个谁来叫? 答案是每个被唤醒的 put 完成入队后, 只要发现还有空位就再 signal 下一个, 唤醒像多米诺一样自己传播下去, take 侧的 c > 1 对 notEmpty 完全对称。
代码块收起展开
为什么不直接用 signalAll? 因为 signalAll 会让全部等待者同时涌起来抢锁, 抢到的只有一个, 其余全部再睡回去, 白白付出 N 次上下文切换; 级联唤醒让每次唤醒都"有位置可用"才发生, 几乎不空转。
后者的条件 `c == 0`(旧值为空)同样是为了省: 跨锁 signal 要先 lock 对面的锁, 是双锁模型里唯一的交叉点, 如果每次 put 都做, 两把锁就退化成一把; 只在"空转非空"这个边沿触发, 是因为只有这一刻才可能有消费者在 notEmpty 上睡——队列非空时来的消费者根本不会睡。注意 signalNotEmpty 在 putLock 释放之后才调用, 进一步缩短了同时持有两把锁的窗口(实际上根本不同时持有)。
最后是那个著名的坑: 默认构造容量是 Integer.MAX_VALUE。Executors.newFixedThreadPool(n) 内部就是 new LinkedBlockingQueue<Runnable>(), 线程数固定, 队列却事实无界——当任务生产速度长期高于消费速度, 任务会无限堆积在队列里, 最终 OOM, 而且在 OOM 之前不会触发任何拒绝策略(队列永远 offer 成功, RejectedExecutionHandler 形同虚设), 也不会创建超过 corePoolSize 的线程(线程池只在队列满时才加线程到 maximumPoolSize)。
这就是阿里规约强制手动 new ThreadPoolExecutor 并显式传有界队列的原因。
设计取舍
- 双锁换吞吐, 代价是复杂度: count 必须原子、跨锁 signal、size() 只能给瞬时值; remove/contains/迭代这类全局操作还要 fullyLock 两把全拿, 比单锁更贵。
- 级联唤醒 + 边沿触发跨锁唤醒, 把 signal 次数压到最少; 代价是逻辑绕——
c + 1 < capacity和c == 0判的都是旧值, 很多人在这里被绕懵。 - 链表按需分配节点, 不用预分配大数组, 但每个元素多一个 Node 对象, GC 压力比 ArrayBlockingQueue 大; dequeue 里的 self-link 就是为了缓解跨代引用。
- ArrayBlockingQueue 支持公平锁参数, LinkedBlockingQueue 的两把锁写死非公平——双锁模型下公平性本来也没法跨锁定义。
- 默认容量 Integer.MAX_VALUE 让”有界队列”变成事实无界, newFixedThreadPool/newSingleThreadExecutor 的 OOM 风险即来源于此; 生产环境一律显式传 capacity。