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

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

在这里插入图片描述

文章目录

  • 从源码全面解析 ArrayBlockingQueue 的来龙去脉
    • 一、引言
    • 二、使用
    • 三、源码
      • 1、初始化
      • 2、生产者的源码
        • 2.1 add()源码实现
        • 2.2 offer()源码实现
        • 2.3 offer(time,unit)源码实现
        • 2.4 put()源码实现
        • 2.5 final ReentrantLock lock = this.lock
        • 2.6 虚假唤醒
      • 3、消费者的源码
        • 3.1 remove()源码实现
        • 3.2 poll() 源码实现
        • 3.3 poll(time,unit)源码实现
        • 3.4 take()源码实现
    • 四、流程图
    • 五、总结

从源码全面解析 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 线程判断数组内还有空余,则放入数组

image-20230422002952714

我们 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();
    }
}

四、流程图

私聊我获取高清流程图

在这里插入图片描述

五、总结

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

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

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

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

我们下期再见。

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

往期文章推荐:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/454692.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

前缀索引,在性能和空间中寻找平衡

文章目录 1.什么是前缀索引2.什么是索引选择性3.创建前缀索引3.1 一个小案例3.2 前缀索引3.3 一个问题 4. 回到开始的问题5.小结 我们在项目的具体实践中&#xff0c;有时候会遇到一些比较特殊的字段&#xff0c;例如身份证号码。 松哥之前有一个小伙伴做黑龙江省的政务服务网&…

手持式激光焊接机多少钱一台

目前很火的一台机器&#xff0c;相比于传统的焊接&#xff0c;手持激光焊接机最大的亮点在于&#xff1a; 1、对于操作工人没有要求&#xff1a;不需要焊工证、男女老少均可 这样可以大大节省人工成本 2、焊接质量 由于他的焊接效率、操作性能比较突出&#xff0c;即便是第一…

C++面试指南——类常用知识点概念总结(附C++进阶视频教程)

构造函数 构造函数可以抛出异常&#xff0c;可以重载&#xff0c;如果在实例化时在类名后面加个括号&#xff0c;只是创建了一个匿名的对象。构造不能是虚函数&#xff0c;因为此时虚函数表还没有初始化。new对象会调解构造函数。没有定义拷贝构造时&#xff0c;IDE会自动生成…

宝塔面板安装配置MySQL,轻松管理数据库【公网远程访问】

文章目录 前言1.Mysql服务安装2.创建数据库3.安装cpolar内网穿透4. 创建HTTP隧道映射mysql端口5.远程连接6.固定TCP地址6.1 保留一个固定的公网TCP端口地址6.2 配置固定公网TCP端口地址 前言 宝塔面板的简易操作性,使得运维难度降低,简化了Linux命令行进行繁琐的配置,下面简单…

软件测试常规测试过程模型——V模型与X模型

一、V模型简单介绍及讲解 V模型是软件测试过程模型中最广为人知的模型&#xff0c;尽管很多富有实际经验的测试人员还是不太熟悉V模型&#xff0c;或者其它的模型。V模型中的过程从左到右&#xff0c;描述了基本的开发过程和测试行为。V模型的价值在于它非常明确地标明了测试过…

SpringBoot整合Minio,一篇带你入门使用Minio

本文介绍SpringBoot如何整合Minio&#xff0c;解决文件存储问题 文章目录 前言环境搭建项目环境搭建添加依赖库yml配置 Docker安装minio 代码实现MiniConfigservicecontroller 测试 前言 参考链接&#xff1a; 官网 环境搭建 项目环境搭建 将minio单独封装成一个module&am…

安全代码审计实施标准学习笔记

声明 本文是学习GB-T 39412-2020 信息安全技术 代码安全审计规范. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 资源使用安全缺陷审计列表 资源管理 审计指标&#xff1a;应避免重复释放资源。 审计人员应检查代码是否存在重复释放资源的情况。重…

Opencv+Python笔记(九)模板匹配

模板匹配 模板匹配常用于对象检测&#xff0c;且实现简单计算效率高。但如果输入图像中存在变化因素如旋转、缩放、视角变化等&#xff0c;模板匹配很容易失效 模板匹配原理&#xff1a; 1.匹配方式为模板 (a * b) 在原图像 (m * n) 上滑动 使用参数method中指定的方法&#…

云原生(docker+k8s+阿里云)

Gitee-Kubernetes学习 kubectl备忘清单 k8s官方文档-task [云原生-kubectl命令详解] ingress详解 ingress官方文档 云原生-语雀-架构师第一课 如上图&#xff0c;服务器有公网ip和私网ip&#xff0c;公网ip是外部访问服务器用的&#xff0c;重启一次实例就变化了&#xff0c;如…

常用数据结构与颜色空间

常用数据结构与颜色空间 矩阵和图像类型 图像可能是灰度&#xff0c;彩色&#xff0c;4 通道的(RGBalpha)&#xff0c;其中每个通道可以包含任意的整数或浮点数。因此&#xff0c;该类型比常见的、易于理解的3通道 8位 RGB 图像更通用。 RBG颜色空间、 HSV/HLS颜色空间、 Lab…

Blender 3.5 面的操作(二)

目录 1. 面操作1.1 面的切割1.2 整体切分1.3 面的法向1.4 正面、背面1.5 翻转法向1.6 填充面1.7 X-Ray 透视模式 1. 面操作 1.1 面的切割 切割工具 Knife&#xff0c;快捷键 k 选中一个面 按k键&#xff0c;进入切割工具&#xff08;建议使用快捷键切割&#xff09;&#xff…

crossover可以安装什么软件?支持的软件列表

CrossOver是一款可以在Mac和Linux等操作系统上运行Windows软件&#xff0c;而无需在计算机上安装Windows操作系统。这款软件的核心技术是Wine&#xff0c;它是一种在Linux和macOS等操作系统上运行Windows应用程序的开源软件。本文将会对CrossOver进行详细介绍&#xff0c;并回答…

“SCSA-T学习导图+”系列:IPSec VPN原理与应用

本期引言&#xff1a; 本章主要讲解IPSec VPN相关理论概念&#xff0c;工作原理。从安全和加密原理入手&#xff0c;讲解了IPSec 在VPN对等体设备实现的安全特性&#xff0c;如数据的机密性、数据的完整性&#xff0c;数据验证等。重点分析IPSec封装模式&#xff0c;IPSec安全…

技术解读丨多模数据湖:助力AI技术,推动内容管理平台智能化升级

随着数字化时代的到来&#xff0c;数据已经成为企业的重要资产之一。因此&#xff0c;构建高效的内容管理平台变得至关重要。本文重点介绍SequoiaDB多模数据湖技术在内容管理平台中的应用和成效&#xff0c;以及其对企业非结构化数据管理和AI的推动作用。 随着数字化时代的到来…

Vue3技术6之toRef和toRefs、shallowReactive与shallowRef、readonly与shallowReadonly

Vue3技术6 toRef和toRefstoRefApp.vueDemo.vue toRefsApp.vueDemoTwo.vue 总结 shallowReactive与shallowRefshallowReactiveApp.vueDemo.vue shallowRefDemo.vue 总结 readonly与shallowReadonlyApp.vueDemo.vueDemoTwo.vue总结 toRef和toRefs toRef App.vue <template&…

SpringCloud入门实战(七)-Hystrix服务限流

&#x1f4dd; 学技术、更要掌握学习的方法&#xff0c;一起学习&#xff0c;让进步发生 &#x1f469;&#x1f3fb; 作者&#xff1a;一只IT攻城狮 。 &#x1f490;学习建议&#xff1a;1、养成习惯&#xff0c;学习java的任何一个技术&#xff0c;都可以先去官网先看看&…

电子行业数字工厂管理系统的生产管理模式是什么

随着电子行业的不断发展&#xff0c;数字工厂管理系统在生产管理中的应用越来越广泛。数字工厂系统是一种综合管理系统&#xff0c;它将企业的采购、生产、销售、财务、人力资源等多个方面进行整合&#xff0c;实现了企业资源的有效整合和管理效率的提升。电子行业数字工厂系统…

vue 使用 threejs 加载第三方模型

threejs 加载第三方模型 接专栏的上一篇博文&#xff0c;这是加载第三方模型相关的。这篇博文拖了很久了哈&#xff0c;简单说一下吧&#xff0c;本来不想写了的&#xff0c;觉得相对来说比较简单&#xff0c;但是还是稍微一扯。为啥要加载第三方呢&#xff0c;上一篇我们绘制的…

人工智能:技术的进步与未来展望

一、引言 1.人工智能的定义 人工智能&#xff08;Artificial Intelligence&#xff0c;简称AI&#xff09;是指由人类创造的具有某种程度上模拟、延伸或超越人类智能的技术。AI技术使计算机能够从数据中学习、推理、适应并执行类似人类大脑所进行的任务。这些任务包括图像识别、…

【Linux命令行与Shell脚本编程】三,Linux文件系统

Linux命令行与Shell脚本编程 第三章 Linux文件系统 文章目录 Linux命令行与Shell脚本编程三.Linux文件系统3.1,查看文件3.1.1,ls 命令 选项和参数3.1.2,过滤输出列表 3.2, 处理文件3.2.1,touch 创建文件3.2.2,cp 复制文件cp -i 覆盖询问cp -R 递归cp命令中使用通配符 3.2.3,ta…