从源码全面解析 ArrayBlockingQueue 的来龙去脉

这篇具有很好参考价值的文章主要介绍了从源码全面解析 ArrayBlockingQueue 的来龙去脉。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

  • 👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主
  • 📕系列专栏:Java设计模式、数据结构和算法、Kafka从入门到成神、Kafka从成神到升仙、Spring从成神到升仙系列
  • 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
  • 🍂博主正在努力完成2023计划中:以梦为马,扬帆起航,2023追梦人
  • 📝联系方式:hls1793929520,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀

从源码全面解析 ArrayBlockingQueue 的来龙去脉

从源码全面解析 ArrayBlockingQueue 的来龙去脉

一、引言

并发编程在互联网技术使用如此广泛,几乎所有的后端技术面试官都要在并发编程的使用和原理方面对小伙伴们进行 360° 的刁难。

作为一个在互联网公司面一次拿一次 Offer 的面霸,打败了无数竞争对手,每次都只能看到无数落寞的身影失望的离开,略感愧疚(请允许我使用一下夸张的修辞手法)。

于是在一个寂寞难耐的夜晚,暖男我痛定思痛,决定开始写 《吊打面试官》 系列,希望能帮助各位读者以后面试势如破竹,对面试官进行 360° 的反击,吊打问你的面试官,让一同面试的同僚瞠目结舌,疯狂收割大厂 Offer

虽然现在是互联网寒冬,但乾坤未定,你我皆是黑马

二、使用

对于阻塞队列,想必大家应该都不陌生,我们这里简单的介绍一下,对于 Java 里面的阻塞队列,其使用了 **生产者和消费者 **的模型

对于生产者来说,主要有以下几部分:

add(E)     	// 添加数据到队列,如果队列满了,无法存储,抛出异常
offer(E)    // 添加数据到队列,如果队列满了,返回false
offer(E,timeout,unit)   // 添加数据到队列,如果队列满了,阻塞timeout时间,如果阻塞一段时间,依然没添加进入,返回false
put(E)      // 添加数据到队列,如果队列满了,挂起线程,等到队列中有位置,再扔数据进去,死等!

对于消费者来说,主要有以下几部分:

remove()    // 从队列中移除数据,如果队列为空,抛出异常
poll()      // 从队列中移除数据,如果队列为空,返回null,么的数据
poll(timeout,unit)   // 从队列中移除数据,如果队列为空,挂起线程timeout时间,等生产者扔数据,再获取
take()     // 从队列中移除数据,如果队列为空,线程挂起,一直等到生产者扔数据,再获取

我们本篇来讲讲堵塞队列中的第一员猛将,ArrayBlockingQueue 的故事

我们简单来写一个小demo

public class ArrayBlockingQueueTest {
    public static void main(String[] args) throws Exception {
        // 必须设置队列的长度
        ArrayBlockingQueue queue = new ArrayBlockingQueue(4);

        // 生产者扔数据
        queue.add("1");
        queue.offer("2");
        queue.offer("3", 2, TimeUnit.SECONDS);
        queue.put("2");

        // 消费者取数据
        System.out.println(queue.remove());
        System.out.println(queue.poll());
        System.out.println(queue.poll(2, TimeUnit.SECONDS));
        System.out.println(queue.take());
    }
}

三、源码

1、初始化

由于我们的 ArrayBlockingQueue 底层使用的是数据结构,所以我们需要在初始化的时候指定其大小,如下:

// 设置其大小长度为 4
ArrayBlockingQueue queue = new ArrayBlockingQueue(4);

// 初始化
public ArrayBlockingQueue(int capacity) {
    this(capacity, false);
}

// 初始化ArrayBlockingQueue的一些初始变量
public ArrayBlockingQueue(int capacity, boolean fair) {
    // 如果传一个负数,直接完蛋
    if (capacity <= 0)
        throw new IllegalArgumentException();
    // 初始化数组items
    this.items = new Object[capacity];
    // 初始化lock非公平锁
    lock = new ReentrantLock(fair);
    // 消费者挂起线程和唤醒线程用到的Condition
    notEmpty = lock.newCondition();
    // 生产者挂起线程和唤醒线程用到的Condition
    notFull =  lock.newCondition();
}

除了我们初始化的这些变量,也有其他的一些变量:

// 存储数据的下标
int putIndex
// 取数据的下标
int takeIndex
// 当前数组中存储的数据长度
int count

对于 ReentrantLocknewCondition 的知识点,可以看以下博文:

  • newCondition的源码分析
  • ReentrantLock的源码分析

2、生产者的源码

2.1 add()源码实现
public boolean add(E e) {
    return super.add(e);
}

// 走到这里会发现,我们的add方法就是调用了offer方法
// offer: 添加数据到队列,如果队列满了,返回false
// 所以这里offer满了,就会抛出异常:"Queue full"
public boolean add(E e) {
    if (offer(e))
        return true;
    else
        throw new IllegalStateException("Queue full");
}
2.2 offer()源码实现
public boolean offer(E e) {
    // 检测当前的入参是否为null
    // 如果是的话直接抛出异常
    checkNotNull(e);
    //【面试绝杀招】为什么会这样引用使用?
    final ReentrantLock lock = this.lock;
    // 直接加锁,保证线程安全
    lock.lock();
    try {
        // 如果当前数组存储的长度等于总容量
        // 直接返回false,插入失败
        if (count == items.length)
            return false;
        else {
            // 插入
            enqueue(e);
            return true;
        }
    } finally {
        // 结束之后将锁释放掉
        lock.unlock();
    }
}

// 添加数据
private void enqueue(E x) {
    // 我们发现,这引用又来了
    final Object[] items = this.items;
    // 当前数组的赋值
    items[putIndex] = x;
    // 如果下标等于我们的总容量,需要重新置下标值为0
    if (++putIndex == items.length)
        putIndex = 0;
    // 数组容量加一
    count++;
    // 唤醒消费者等待的线程
    notEmpty.signal();
}
2.3 offer(time,unit)源码实现

生产者在添加数据时,如果队列已经满了,阻塞一会

  • 阻塞到消费者消费了消息,然后唤醒当前阻塞线程
  • 阻塞到了 time 时间,再次判断是否可以添加,不能,直接告辞
public boolean offer(E e, long timeout, TimeUnit unit)throws InterruptedException {
    // 检测当前的入参是否为空
    checkNotNull(e);
    // 统一时间格式
    long nanos = unit.toNanos(timeout);
    // 持有引用
    final ReentrantLock lock = this.lock;
    // 允许的中断的加锁方式
    lock.lockInterruptibly();
    try {
        // 如果当前的存储等于数组的长度
        // 这里为什么不能用if判断,需要用while,牵扯到虚假唤醒,我们后面聊
        while (count == items.length) {
            // 时间小于0,直接返回false
            if (nanos <= 0)
                return false;
            // 挂起当前线程
            nanos = notFull.awaitNanos(nanos);
        }
        // 添加到数组中
        enqueue(e);
        return true;
    } finally {
        // 解锁
        lock.unlock();
    }
}
2.4 put()源码实现
public void put(E e) throws InterruptedException {
    // 检测当前的入参是否为空
    checkNotNull(e);
    // 持有引用
    final ReentrantLock lock = this.lock;
    // 允许的中断的加锁方式
    lock.lockInterruptibly();
    try {
        // 如果当前的存储等于数组的长度
        // 这里为什么不能用if判断,需要用while,牵扯到虚假唤醒,我们后面聊
        while (count == items.length)
            // 无时间挂起当前线程
            notFull.await();
        // 添加到队列
        enqueue(e);
    } finally {
        // 解锁
        lock.unlock();
    }
}

通过上面的源码分析,我们应该可以理解上面说的这几句话了

add(E)     	// 添加数据到队列,如果队列满了,无法存储,抛出异常
offer(E)    // 添加数据到队列,如果队列满了,返回false
offer(E,timeout,unit)   // 添加数据到队列,如果队列满了,阻塞timeout时间,如果阻塞一段时间,依然没添加进入,返回false
put(E)      // 添加数据到队列,如果队列满了,挂起线程,等到队列中有位置,再扔数据进去,死等!

接着我们讲两个小细节,也是面试震惊面试官的地方

2.5 final ReentrantLock lock = this.lock

在我们 Doug Lea 里写的代码中,java.util.concurrent 包下 和 HashMap 中都有类似的写法

这种写法到底有什么好处呢,为什么我们不能直接使用成员变量 lock 来进行加锁解锁

感兴趣的可以看下这篇博文:原因

不感兴趣的可以看我的总结分析:

首先我们需要准备下面两个代码,将其反编译得到 Java 字节码

  • 引用状态

    public void get1(){
        final ReentrantLock lock = this.lock;
        lock.lock();
    }
    
  • 非引用状态

    public void get2(){
        lock.lock();
    }
    

通过对比字节码我们发现,引用状态的字节码相较于非引用状态少了一个指令:getfield

而这个缺少的指令,也正是 Doug Lea 优化的来源:从栈读变量比从堆读变量会更cache-friendly,本地变量最终绑定到CPU寄存器的可能性更高。

但由于现在的 Java 编译器已经非常先进了,不论采用哪种方式,最终形成的机器指令都是一样的

所以,Doug Lea 的优化在之前是字节码层面的优化,但如今确实没有卵用

2.6 虚假唤醒

我们上面有一段 while 循环的代码:

// 如果当前的存储等于数组的长度
// 这里为什么不能用if判断,需要用while,牵扯到虚假唤醒,我们后面聊
while (count == items.length) {
    // 无时间挂起当前线程
    notFull.await();
}
// 添加到队列
enqueue(e);

我们 A 线程判断数组内还有空余,则放入数组

从源码全面解析 ArrayBlockingQueue 的来龙去脉

我们 B 线程判断其 count == items.length 进入挂起状态,当我们的 B 线程被唤醒时,如果不经历 count == items.length 的过程,就会将我们 A 线程的 3 数据给覆盖掉

3、消费者的源码

3.1 remove()源码实现
  • 主要使用了我们的poll方法
public E remove() {
    // 直接调用poll方法
    E x = poll();
    // 如果有数据则返回
    // 无数据则抛出异常
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}
3.2 poll() 源码实现
public E poll() {
    // 还是引用
    final ReentrantLock lock = this.lock;
    // 锁一下
    lock.lock();
    try {
        // 判断数组容量是否等于0(数组无容量),返回null
        // 如果数组中有数据,则进行dequeue方法
        return (count == 0) ? null : dequeue();
    } finally {
        // 解锁
        lock.unlock();
    }
}

private E dequeue() {
    // 还是引用
    final Object[] items = this.items;
    // 当前弹出数组的下标
    E x = (E) items[takeIndex];
    // 弹出后将当前下标的数据置为空
    items[takeIndex] = null;
    // 如果我们的弹出下标和我们数组的大小一样时,需要更新弹出下标
    if (++takeIndex == items.length)
        takeIndex = 0;
    // 数组数据数量减一
    count--;
    // 迭代器内容,先忽略
    if (itrs != null)
        itrs.elementDequeued();
    // 唤醒生产者的线程
    notFull.signal();
    return x;
}
3.3 poll(time,unit)源码实现
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    // 将时间转化成统一单位
    long nanos = unit.toNanos(timeout);
    // 引用
    final ReentrantLock lock = this.lock;
    // 可中断的加锁
    lock.lockInterruptibly();
    try {
        // 看一下当前的数组还有容量没
        while (count == 0) {
            // 如果没有容量并且时间也到期了,返回null
            if (nanos <= 0)
                return null;
            // 进入带有时间的等待状态(扔到Condition队列中)
            nanos = notEmpty.awaitNanos(nanos);
        }
        // 被唤醒后并且当前的数组有容量
        // 弹出队列中的数据即可
        return dequeue();
    } finally {
        // 解锁
        lock.unlock();
    }
}
3.4 take()源码实现
public E take() throws InterruptedException {
    // 引用
    final ReentrantLock lock = this.lock;
    // 可中断的加锁
    lock.lockInterruptibly();
    try {
        // 看一下当前的数组还有容量没
        while (count == 0){
            // 没容量直接扔Condition队列等待
            notEmpty.await();
        }
        // 被唤醒后并且当前的数组有容量
        // 弹出队列中的数据即可
        return dequeue();
    } finally {
        // 解锁
        lock.unlock();
    }
}

四、流程图

私聊我获取高清流程图

从源码全面解析 ArrayBlockingQueue 的来龙去脉

五、总结

鲁迅先生曾说:独行难,众行易,和志同道合的人一起进步。彼此毫无保留的分享经验,才是对抗互联网寒冬的最佳选择。

其实很多时候,并不是我们不够努力,很可能就是自己努力的方向不对,如果有一个人能稍微指点你一下,你真的可能会少走几年弯路。

如果你也对 后端架构和中间件源码 有兴趣,欢迎添加博主微信:hls1793929520,一起学习,一起成长

我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,喜欢后端架构和中间件源码。

我们下期再见。

我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。

往期文章推荐:文章来源地址https://www.toymoban.com/news/detail-454668.html

  • 从根上剖析ReentrantLock的来龙去脉
  • 阅读完synchronized和ReentrantLock的源码后,我竟发现其完全相似
  • 从源码全面解析 ThreadLocal 关键字的来龙去脉
  • 从源码全面解析 synchronized 关键字的来龙去脉
  • 阿里面试官让我讲讲volatile,我直接从HotSpot开始讲起,一套组合拳拿下面试

到了这里,关于从源码全面解析 ArrayBlockingQueue 的来龙去脉的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 从源码全面解析Java 线程池的来龙去脉

    👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主 📕系列专栏:Java设计模式、Spring源码系列、Netty源码系列、Kafka源码系列、JUC源码系列 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦 🍂博主正在努

    2024年02月03日
    浏览(60)
  • 从源码全面解析 dubbo 消费端服务调用的来龙去脉

    👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主 📕系列专栏:Java设计模式、Spring源码系列、Netty源码系列、Kafka源码系列、JUC源码系列、duubo源码系列 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦

    2024年02月09日
    浏览(37)
  • 从源码全面解析 dubbo 服务端服务调用的来龙去脉

    👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主 📕系列专栏:Java设计模式、Spring源码系列、Netty源码系列、Kafka源码系列、JUC源码系列、duubo源码系列 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦

    2024年02月11日
    浏览(33)
  • 《吊打面试官系列》从源码全面解析 ThreadLocal 关键字的来龙去脉

    👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主 📕系列专栏:Java设计模式、数据结构和算法、Kafka从入门到成神、Kafka从成神到升仙、Spring从成神到升仙系列 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一

    2023年04月23日
    浏览(67)
  • 《吊打面试官系列》从源码全面解析 synchronized 关键字的来龙去脉

    👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主 📕系列专栏:Java设计模式、数据结构和算法、Kafka从入门到成神、Kafka从成神到升仙、Spring从成神到升仙系列 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一

    2023年04月16日
    浏览(47)
  • 【Spring从成神到升仙系列 四】从源码分析 Spring 事务的来龙去脉

    👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主 📕系列专栏:Java设计模式、数据结构和算法、Kafka从入门到成神、Kafka从成神到升仙、Spring从成神到升仙系列 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一

    2024年02月03日
    浏览(34)
  • 二次型的来龙去脉

            在学习二次型的时候没有好好理解概念,导致记住了可以用的结论,但往往遇到题目反应不过来,故这次对二次型进行一个详细剖析。         首先二次型是什么?是一个n元变量的二次齐次多项式,根据二次齐次多项式的定义(所有单项的次数都是2,单项的次数为

    2024年02月09日
    浏览(39)
  • 简单聊聊Https的来龙去脉

    使用明文通信,通信内容可能会被监听 不验证通信双方身份,因此可能会遭遇伪装 无法验证报文完整性,可能会遭到中间人攻击,从而篡改请求和响应报文中的内容 Http 协议直接和TCP进行通信,而 Https 在 Http 和 Tcp 之间加了一层 SSL 实现加密传输 : SSL ( Secure Socket Layer ) 安全

    2024年02月10日
    浏览(43)
  • 一文解释mapState的来龙去脉

    mapState Vuex 提供的辅助函数之一,将 store 中的状态映射到组件的计算属性中,使得在组件中可以轻松地访问 Vuex store 中的状态值 MapState(映射状态) 在我们的 Count.vue 组件中,可以使用 mapState 来更简洁地获取 count 的状态值 首先,导入 mapState : 然后,在 computed 中使用 mapState :

    2024年02月07日
    浏览(40)
  • 你所不知道的 数据在内存中储存 的来龙去脉

            那么好了好了,宝子们,今天给大家介绍一下 “数据在内存中储存” 的来龙去脉,来吧,开始整活!⛳️          一、数据类型的介绍 (1)整型和浮点型:    (2)其他类型:    二、数据在内存中的储存顺序(大端 小端)   (1)引入字节序: 字节序

    2024年02月06日
    浏览(68)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包