文章目录

📙 作者: 编程技术圈(哇哥面试陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕tcmeta, 欢迎沟通交流
零、引入
“面试官就问了一句‘ArrayBlockingQueue 底层咋实现阻塞的?’,我就只说‘能等,满了等空了等’,结果面试官直接让我回去等通知……” 王二耷拉着脑袋,手里的面试反馈单上写着 “底层原理掌握不足”,刚面的大厂 offer 就这么飞了。
隔壁哇哥嚼着口香糖,一把抢过反馈单:“你光会用有啥用?面试考的是底层!阻塞队列的核心就是 ReentrantLock+Condition—— 锁保证线程安全,Condition 实现精准阻塞 / 唤醒,今天我手撕 ArrayBlockingQueue 源码,再带你手写一个简易版,下次面试让你把面试官说懵。”
点赞 + 关注,跟着哇哥和王二,从源码到手写,彻底搞懂阻塞队列的底层实现,面试多拿 10k!

一、先搞懂核心需求:阻塞队列要解决啥问题?

哇哥先给王二划重点:“阻塞队列就俩核心操作,必须线程安全还得能阻塞:
- put():队列满了,生产者线程阻塞,直到队列有空位;
- take():队列空了,消费者线程阻塞,直到队列有元素。
这俩需求,光用 synchronized+wait/notify 能实现,但麻烦还容易出问题;而 ReentrantLock+Condition 是最优解 —— 锁保证操作互斥,Condition 实现精准的 “满 / 空” 通知。”
✔️ 用食堂打饭理解 ReentrantLock+Condition

“还是用食堂打饭的例子,” 哇哥打比方,“ReentrantLock 就是打饭窗口的‘大门锁’,只有拿到锁的人才能进窗口操作;Condition 分两个:
- notFull(没满):相当于窗口阿姨喊‘有位置了,快来打饭’,唤醒阻塞的生产者;
- notEmpty(没空):相当于阿姨喊‘有饭了,快来取’,唤醒阻塞的消费者。
对比 Object 的 wait/notify:wait/notify 是‘广播’,喊一嗓子所有人都醒,可能消费者醒了但队列还是空的,白醒;而 Condition 是‘精准喊话’,该喊生产者喊生产者,该喊消费者喊消费者,效率高多了。”
二、手撕 ArrayBlockingQueue 源码:核心就这几行
哇哥打开 IDE,调出 ArrayBlockingQueue 的核心源码,给王二逐行拆解 —— 其实底层逻辑特别简单,核心就一个 ReentrantLock + 两个 Condition。

📌 第一步:核心成员变量(锁 + 两个 Condition)
// ArrayBlockingQueue核心成员变量
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
// 存储元素的数组(底层就是数组,所以是有界队列)
final Object[] items;
// 取元素的索引
int takeIndex;
// 放元素的索引
int putIndex;
// 元素数量
int count;
// 核心锁:保证put/take操作的线程安全
final ReentrantLock lock;
// 消费者的Condition:队列不空时唤醒消费者
private final Condition notEmpty;
// 生产者的Condition:队列不满时唤醒生产者
private final Condition notFull;
// 构造方法:初始化锁和Condition
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
// 初始化锁(可指定公平/非公平,默认非公平)
lock = new ReentrantLock(fair);
// 基于锁创建两个Condition
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
}
哇哥敲着代码解释:“看到没?ArrayBlockingQueue 底层就是数组 + ReentrantLock + 两个 Condition—— 锁保证 put 和 take 互斥,两个 Condition 分别管‘满了等’和‘空了等’。”
🚀 第二步:put () 方法源码(生产者阻塞逻辑)
// put():阻塞放入元素,满了就等
public void put(E e) throws InterruptedException {
// 检查元素非空
checkNotNull(e);
final ReentrantLock lock = this.lock;
// 加锁(可中断,响应线程中断)
lock.lockInterruptibly();
try {
// 核心:队列满了,生产者await(阻塞)
while (count == items.length)
notFull.await(); // 释放锁,等待notFull的signal
// 队列没满,入队
enqueue(e);
} finally {
// 解锁
lock.unlock();
}
}
// 入队操作(私有方法,已加锁)
private void enqueue(E x) {
final Object[] items = this.items;
// 把元素放到putIndex位置
items[putIndex] = x;
// 索引循环(数组满了就回到0,环形数组)
if (++putIndex == items.length)
putIndex = 0;
// 元素数量+1
count++;
// 唤醒等待的消费者(队列从空变非空了)
notEmpty.signal();
}
📢 关键拆解(哇哥划重点)
- lockInterruptibly():可中断加锁,比 lock () 灵活 —— 如果线程在等锁时被中断,会抛 - InterruptedException,不会死等;
- while (count == items.length):用 while 而不是 if!防止 “虚假唤醒”(比如多个生产者被唤醒,只有一个能入队,剩下的要再检查队列是否满);
- notFull.await():生产者释放锁,进入阻塞状态,直到被 notFull.signal () 唤醒;
- enqueue 后 signal ():放入元素后,队列从空变非空,唤醒等待的消费者。
🎸 第三步:take () 方法源码(消费者阻塞逻辑)
// take():阻塞取出元素,空了就等
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 核心:队列空了,消费者await(阻塞)
while (count == 0)
notEmpty.await();
// 队列非空,出队
return dequeue();
} finally {
lock.unlock();
}
}
// 出队操作(私有方法,已加锁)
private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
// 取出takeIndex位置的元素
E x = (E) items[takeIndex];
// 清空该位置,避免内存泄漏
items[takeIndex] = null;
// 索引循环
if (++takeIndex == items.length)
takeIndex = 0;
// 元素数量-1
count--;
// 唤醒等待的生产者(队列从满变不满了)
notFull.signal();
return x;
}
🔰 王二恍然大悟:“原来阻塞的核心是 Condition 的 await/signal!”
“对!” 哇哥点头,“put 满了就等 notFull,take 空了就等 notEmpty;入队后唤醒 notEmpty,出队后唤醒 notFull—— 精准通知,不浪费 CPU,这就是 Condition 比 wait/notify 牛的地方。”
三、手写简易版阻塞队列:吃透核心逻辑

为了让王二彻底掌握,哇哥带着他手写了一个简易版的阻塞队列,只保留核心的 put/take,去掉复杂的索引循环,逻辑更清晰。
💯 手写阻塞队列完整代码
package cn.tcmeta.blockingqueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
// 手写简易版阻塞队列:核心ReentrantLock+Condition
public class MyBlockingQueue<T> {
// 存储元素的数组(有界)
private final Object[] queue;
// 队列大小
private int size;
// 核心锁
private final ReentrantLock lock;
// 消费者Condition:队列不空时唤醒
private final Condition notEmpty;
// 生产者Condition:队列不满时唤醒
private final Condition notFull;
// 构造方法:指定队列容量
public MyBlockingQueue(int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException("容量必须大于0");
}
queue = new Object[capacity];
lock = new ReentrantLock();
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
// 阻塞放入元素(核心)
public void put(T t) throws InterruptedException {
// 1. 加锁(可中断)
lock.lockInterruptibly();
try {
// 2. 队列满了,生产者阻塞
while (size == queue.length) {
System.out.println("队列满了,生产者" + Thread.currentThread().getName() + "阻塞等待...");
notFull.await(); // 释放锁,等待唤醒
}
// 3. 队列没满,放入元素
queue[size] = t;
size++;
System.out.println("生产者" + Thread.currentThread().getName() + "放入:" + t + ",当前队列大小:" + size);
// 4. 唤醒阻塞的消费者(队列从空变非空)
notEmpty.signal();
} finally {
// 5. 解锁
lock.unlock();
}
}
// 阻塞取出元素(核心)
public T take() throws InterruptedException {
// 1. 加锁
lock.lockInterruptibly();
try {
// 2. 队列空了,消费者阻塞
while (size == 0) {
System.out.println("队列空了,消费者" + Thread.currentThread().getName() + "阻塞等待...");
notEmpty.await();
}
// 3. 队列非空,取出第一个元素
@SuppressWarnings("unchecked")
T t = (T) queue[0];
// 4. 数组前移(简易版,不做环形索引)
for (int i = 0; i < size - 1; i++) {
queue[i] = queue[i + 1];
}
queue[size - 1] = null; // 清空最后一个位置
size--;
System.out.println("消费者" + Thread.currentThread().getName() + "取出:" + t + ",当前队列大小:" + size);
// 5. 唤醒阻塞的生产者(队列从满变不满)
notFull.signal();
return t;
} finally {
// 6. 解锁
lock.unlock();
}
}
// 测试代码
public static void main(String[] args) {
// 创建容量为3的阻塞队列
MyBlockingQueue<String> queue = new MyBlockingQueue<>(3);
// 启动3个生产者线程:放5个元素(会阻塞)
for (int i = 1; i <= 5; i++) {
int finalI = i;
new Thread(() -> {
try {
queue.put("订单" + finalI);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "生产者" + i).start();
}
// 等2秒,让生产者放满3个元素,剩下的2个阻塞
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 启动2个消费者线程:取5个元素(会阻塞)
for (int i = 1; i <= 2; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 3; j++) { // 每个消费者取3个,总共取6个(最后一个会阻塞)
queue.take();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "消费者" + i).start();
}
}
}
执行结果:
生产者生产者1放入:订单1,当前队列大小:1
生产者生产者2放入:订单2,当前队列大小:2
生产者生产者3放入:订单3,当前队列大小:3
队列满了,生产者生产者4阻塞等待...
队列满了,生产者生产者5阻塞等待...
消费者消费者2取出:订单1,当前队列大小:2
消费者消费者1取出:订单2,当前队列大小:1
消费者消费者1取出:订单3,当前队列大小:0
队列空了,消费者消费者1阻塞等待...
生产者生产者4放入:订单4,当前队列大小:1
生产者生产者5放入:订单5,当前队列大小:2
消费者消费者2取出:订单4,当前队列大小:1
消费者消费者2取出:订单5,当前队列大小:0
队列空了,消费者消费者1阻塞等待...
王二跑完代码,拍着大腿:“终于懂了!put 满了等 notFull,take 空了等 notEmpty,唤醒也是精准的 —— 这就是阻塞队列的底层核心啊!”

四、ReentrantLock+Condition vs synchronized+wait/notify
哇哥给王二对比了两种实现方式的优劣,帮他理清面试思路:

面试时被问‘为什么阻塞队列用 ReentrantLock+Condition’,就答这几点 —— 精准通知、可中断、灵活,这是核心优势。”
五、总结:阻塞队列底层的核心心法
哇哥把核心要点缩成 3 句顺口溜,王二抄在面试笔记本上:
- 锁保证互斥:ReentrantLock 保证 put/take 操作同一时间只有一个线程执行,线程安全;
- Condition 管阻塞:notEmpty 管消费者(空了等),notFull 管生产者(满了等),精准唤醒不浪费;
- while 防虚假唤醒:阻塞判断用 while 而不是 if,避免多线程唤醒后条件不满足;
- 解锁放 finally:不管是否抛异常,锁都要释放,避免锁泄露。
🎲 哇哥的血泪彩蛋

“我早年手写阻塞队列,把 Condition 的 signal 写反了 —— 入队后唤醒 notFull,出队后唤醒 notEmpty,” 哇哥捂脸,“结果生产者满了阻塞后,永远醒不过来,测试库直接卡死,查了 3 小时才发现是 signal 写反了 —— 从那以后,我写 Condition 必先画‘谁等谁、谁唤醒谁’的图!”
💯 最后说句实在的

阻塞队列的底层不是啥高深技术,核心就是 ReentrantLock 保证线程安全,Condition 实现精准的阻塞 / 唤醒。记住:锁是 “大门”,保证只有一个人进门操作;Condition 是 “喊话器”,该喊谁就喊谁,不瞎喊。
今天手写的简易版阻塞队列,你可以直接拿去面试手撕 —— 比光背源码强 10 倍!如果这篇帮你搞定了面试难题,点赞 + 分享给你那还在背 “阻塞队列能等” 的同事!



被折叠的 条评论
为什么被折叠?



