【028】面试被问阻塞队列底层?王二支支吾吾被刷,哇哥手撕源码,用 ReentrantLock+Condition 讲透!

在这里插入图片描述

📙 作者: 编程技术圈(哇哥面试陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕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 倍!如果这篇帮你搞定了面试难题,点赞 + 分享给你那还在背 “阻塞队列能等” 的同事!

请添加图片描述
请添加图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值