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

news/2024/5/2 5:51:37/文章来源:https://blog.csdn.net/qq_40915439/article/details/130333225
  • 👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的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();// 初始化数组itemsthis.items = new Object[capacity];// 初始化lock非公平锁lock = new ReentrantLock(fair);// 消费者挂起线程和唤醒线程用到的ConditionnotEmpty = lock.newCondition();// 生产者挂起线程和唤醒线程用到的ConditionnotFull =  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;elsethrow 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;// 如果下标等于我们的总容量,需要重新置下标值为0if (++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,直接返回falseif (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;elsethrow 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) {// 如果没有容量并且时间也到期了,返回nullif (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.luyixian.cn/news_show_102639.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

安卓手机(微信小程序)抓蓝牙通信数据包

前言 因为公司需要......所以我就弄了一下,参考了很多别人的文章。 成果:它可以抓取微信小程序、安卓APP的蓝牙数据通信包。 开始 我是小米手机,所以我以我自己手机为例 通信过程操作 第一步 打开开发者选项,打开蓝牙调试日志和蓝牙数据包日志开关(如果两者只有其中…

MAVEN环境变量配置(Windows 11)

1、直接在搜索框中搜&#xff1a;编辑系统环境变量 2、点击环境变量 3、 在系统变量里面新建系统变量 变量名&#xff1a;MAVEN_HOME 变量值&#xff1a;路径一定要写到maven的bin目录下 以下这种写法是错误的 4、新建系统变量完成 5、 往下滑 找到path&#xff0c;可以双击…

什么是gpt一4-如何用上gpt-4

怎么使用gpt-4 目前GPT-4还未正式发布或公开&#xff0c;因此也没有详细的对接说明。但是我们可以根据GPT-4的前身GPT-3的应用经验&#xff0c;以及GPT-4的预期功能推测一些可能的使用步骤&#xff1a; 选择适合的GPT-4实现技术&#xff1a;GPT-4可能有不同的实现技术&#xff…

Opencv+Python笔记(八)轮廓检测

目录 一、轮廓的检测和绘制1.读入图像2.将读入图像转化为灰度图3.对灰度图进行二值化 [图像的阈值化处理](https://blog.csdn.net/Ggs5s_/article/details/130301816?spm1001.2014.3001.5501)4.进行轮廓检测5.在原图中显示轮廓 二、轮廓层级关系1.RET_LIST2.RETR_EXTERNAL3. R…

慈航公益·高莞乡情行

2023年4月9日&#xff0c;慈航公益高莞乡情行---高村陈屋第二届“敬老睦宗情暖乡土”活动日暨“英华教育奖学基金”成立大会在高村陈屋如期举行。 高村陈屋位于河源市连平县境内&#xff0c;为了赶上10点开始的活动&#xff0c;清晨六点半&#xff0c;志愿者们便从慈航出发&am…

Dynamic Slicing for Deep Neural Networks

0、摘要 程序切片已广泛应用于各种软件工程任务中。然而&#xff0c;现有的程序切片技术只能处理由指令和变量构建的传统程序&#xff0c;而不能处理由神经元和突触组成的神经网络。在本文中&#xff0c;我们提出了 NNSlicer&#xff0c;这是第一种基于数据流分析的深度神经网络…

【ONE·C++ || 继承】

总言 主要介绍继承相关内容。 文章目录 总言1、继承介绍1.1、继承是什么1.2、继承方式与访问限定符1.3、继承作用域 2、基类和派生类对象赋值转换2.1、子类对象可以赋值给父类对象/指针/引用2.2、基类对象不能赋值给派生类对象2.3、基类的指针可以通过强制类型转换赋值给派生类…

vue2的生命周期

生命周期就是记录数据的状态。对数据进行操作 刚开始 new Vue() 创建了一个实例对象 beforeCreate() 数据还没有创建出来 created() 数据创建出来了&#xff0c;可以访问 判断有没有el 或 template 后 将模板编译成渲染函数 beforeMount() 数据还没有挂在到页面上面 mou…

深度强化学习——蒙特卡洛算法(6)

注&#xff1a;本章的内容作为补充插曲&#xff0c;大家可以选看&#xff0c;不过还是建议把最后一个使用蒙特卡洛近似求期望稍微看一下 蒙特卡洛是一大堆随机算法&#xff0c;通过随机样本来估算真实值 使用随机样本来近似Π 1、在[a,b]做随机均匀抽样&#xff0c;抽出n个样…

数据库物理存储结构

目录 一、数据库文件和文件组 1、数据库文件 &#xff08;1&#xff09; 主数据库文件&#xff08;Primary Database File&#xff09; &#xff08;2&#xff09; 次数据库文件&#xff08;Secondary Database File&#xff09; &#xff08;3&#xff09; 事务日志文件 …

Mysql 45讲和45问笔记(未完待续0203/04/24)

一、mysql 45讲 1&#xff09;索引的本质讲解 定义解释 所以是帮助Mysql高效获取数据的排好序的数据结构 索引数据结构 ①二叉树 ②红黑树 ③Hash表 ④B-Tree 原理讲解 可以看到右边的数据结构里面&#xff0c;是按照k-v来存数据结构的&#xff0c;key是col2的字段&#xf…

Java学习-MySQL-事务

Java学习-MySQL-事务 ACID原则&#xff1a;原子性、一致性、隔离性、持久性 原子性&#xff08;Atomicity&#xff09; 两个步骤要么一起成功&#xff0c;要么一起失败&#xff0c;不可能只成功一个。 举例&#xff1a; A账户400元&#xff0c;B账户600元&#xff0c;A向B转…

Yolo v1 笔记

个人不太懂的点 1.损失函数的4与5项 【论文解读】Yolo三部曲解读——Yolov1 - 知乎 https://www.youtube.com/watch?vNkFENlEb4kM&t672s 训练阶段&#xff1a; C_i 预测值&#xff1a;由网络输出出来7*7*30中第一个bbox和第二个bbox的置信度confidence C_i^hat 标签值…

PTA L2-046 天梯赛的赛场安排 (25 分)

天梯赛使用 OMS 监考系统&#xff0c;需要将参赛队员安排到系统中的虚拟赛场里&#xff0c;并为每个赛场分配一位监考老师。每位监考老师需要联系自己赛场内队员对应的教练们&#xff0c;以便发放比赛账号。为了尽可能减少教练和监考的沟通负担&#xff0c;我们要求赛场的安排满…

C++ 类和对象(中)构造函数 和 析构函数

上篇链接&#xff1a;C 类和对象&#xff08;上&#xff09;_chihiro1122的博客-CSDN博客 类的6个默认成员函数 我们在C当中&#xff0c;在写一些函数的时候&#xff0c;比如在栈的例子&#xff1a; 如上述例子&#xff0c;用C 返回这个栈是否为空&#xff0c;直接返回的话&am…

利用Python操作Mysql数据库

我们在进行Python编程的时候&#xff0c;时常要将一些数据保存起来&#xff0c;其中最方便的莫过于保存在文本文件了。但是如果保存的文件太大&#xff0c;用文本文件就不太现实了&#xff0c;毕竟打开都是个问题&#xff0c;这个时候我们需要用到数据库。提到数据库&#xff0…

json模块和pickle模块

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起探讨和分享Linux C/C/Python/Shell编程、机器人技术、机器学习、机器视觉、嵌入式AI相关领域的知识和技术。 json和pickle模块 json模块序列化与反序列化json模块中的方法 pickle模块 专栏&#xff1a;《python从…

JAVAweb开发学习

六、MybatisPlus快速上手 数据库操作 注意&#xff01;注意&#xff01;注意&#xff01;springboot版本选择2.7.2 1.ORM介绍&#xff08;对象关系映射&#xff09; 既包含存储&#xff0c;又包含映射。将java类映射到数据库 2.MybatisPlus介绍 ORM框架 数据库操作来啦…

【计算机网络】为什么 TCP 每次建立连接时,初始化序列号都要不一样呢?

【计算机网络】为什么 TCP 每次建立连接时&#xff0c;初始化序列号都要不一样呢&#xff1f; 为什么 TCP 每次建立连接时&#xff0c;初始化序列号都要不一样呢&#xff1f; 主要原因是为了防止历史报文被下一个相同四元组的连接接收。 TCP 四次挥手中的 TIME_WAIT 状态不是会…