我们平时开发中好像很少使用到BlockingQueue(阻塞队列),比如我们想要存储一组数据的时候会使用ArrayList,想要存储键值对数据会使用HashMap,在什么场景下需要用到BlockingQueue呢?

1. BlockingQueue的应用场景

当我们处理完一批数据之后,需要把这批数据发给下游方法接着处理,但是下游方法的处理速率不受控制,可能时快时慢。如果下游方法的处理速率较慢,会拖慢当前方法的处理速率,这时候该怎么办呢?

你可能想到使用线程池,是个办法,不过需要创建很多线程,还要考虑下游方法支不支持并发,如果是CPU密集任务,可能多线程比单线程处理速度更慢,因为需要频繁上下文切换。

这时候就可以考虑使用BlockingQueue,BlockingQueue最典型的应用场景就是上面这种生产者-消费者模型。生产者往队列中放数据,消费者从队列中取数据,中间使用BlockingQueue做缓冲队列,也就解决了生产者和消费者速率不同步的问题。

你可能联想到了消息队列(MessageQueue),消息队列相当于分布式阻塞队列,而BlockingQueue相当于本地阻塞队列,只作用于本机器。对应的是分布式缓存(比如:Redis、Memcache)和本地缓存(比如:Guava、Caffeine)。

另外很多框架中都有BlockingQueue的影子,比如线程池中就用到BlockingQueue做任务的缓冲。消息队列中发消息、拉取消息的方法也都借鉴了BlockingQueue,使用起来很相似。

今天就一块深入剖析一下Queue的底层源码。

2. BlockingQueue的用法

BlockingQueue的用法非常简单,就是放数据和取数据。

/**
* @apiNote BlockingQueue示例
* @author 一灯架构
*/
public class Demo {
public static void main(String[] args) throws InterruptedException {
// 1. 创建队列,设置容量是10
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
// 2. 往队列中放数据
queue.put(1);
// 3. 从队列中取数据
Integer result = queue.take();
}
}

为了满足不同的使用场景,BlockingQueue设计了很多的放数据和取数据的方法。

操作 抛出异常 返回特定值 阻塞 阻塞一段时间
放数据 add offer put offer(e, time, unit)
取数据 remove poll take poll(time, unit)
取数据(不删除) element() peek() 不支持 不支持

这几组方法的不同之处就是:

  1. 当队列满了,再往队列中放数据,add方法抛异常,offer方法返回false,put方法会一直阻塞(直到有其他线程从队列中取走数据),offer方法阻塞指定时间然后返回false。
  2. 当队列是空,再从队列中取数据,remove方法抛异常,poll方法返回null,take方法会一直阻塞(直到有其他线程往队列中放数据),poll方法阻塞指定时间然后返回null。
  3. 当队列是空,再去队列中查看数据(并不删除数据),element方法抛异常,peek方法返回null。

工作中使用最多的就是offer、poll阻塞指定时间的方法。

3. BlockingQueue实现类

BlockingQueue常见的有下面5个实现类,主要是应用场景不同。

  • ArrayBlockingQueue

    基于数组实现的阻塞队列,创建队列时需指定容量大小,是有界队列。

  • LinkedBlockingQueue

    基于链表实现的阻塞队列,默认是无界队列,创建可以指定容量大小

  • SynchronousQueue

    一种没有缓冲的阻塞队列,生产出的数据需要立刻被消费

  • PriorityBlockingQueue

    实现了优先级的阻塞队列,基于数据显示,是无界队列

  • DelayQueue

    实现了延迟功能的阻塞队列,基于PriorityQueue实现的,是无界队列

4. BlockingQueue源码解析

BlockingQueue的5种子类实现方式大同小异,这次就以最常用的ArrayBlockingQueue做源码解析。

4.1 ArrayBlockingQueue类属性

先看一下ArrayBlockingQueue类里面有哪些属性:

// 用来存放数据的数组
final Object[] items; // 下次取数据的数组下标位置
int takeIndex; // 下次放数据的数组下标位置
int putIndex; // 当前已有元素的个数
int count; // 独占锁,用来保证存取数据安全
final ReentrantLock lock; // 取数据的条件
private final Condition notEmpty; // 放数据的条件
private final Condition notFull;

ArrayBlockingQueue中4组存取数据的方法实现也是大同小异,本次以put和take方法进行解析。

4.2 put方法源码解析

无论是放数据还是取数据都是从队头开始,逐渐往队尾移动。

// 放数据,如果队列已满,就一直阻塞,直到有其他线程从队列中取走数据
public void put(E e) throws InterruptedException {
// 校验元素不能为空
checkNotNull(e);
final ReentrantLock lock = this.lock;
// 加锁,加可中断的锁
lock.lockInterruptibly();
try {
// 如果队列已满,就一直阻塞,直到被唤醒
while (count == items.length)
notFull.await();
// 如果队列未满,就往队列添加元素
enqueue(e);
} finally {
// 结束后,别忘了释放锁
lock.unlock();
}
} // 实际往队列添加数据的方法
private void enqueue(E x) {
// 获取数组
final Object[] items = this.items;
// putIndex 表示本次插入的位置
items[putIndex] = x;
// ++putIndex 计算下次插入的位置
// 如果本次插入的位置,正好是队尾,下次插入就从 0 开始
if (++putIndex == items.length)
putIndex = 0;
// 元素数量加一
count++;
// 唤醒因为队列空等待的线程
notEmpty.signal();
}

源码中有个有意思的设计,添加元素的时候如果已经到了队尾,下次就从队头开始添加,相当于做成了一个循环队列。

像下面这样:

4.3 take方法源码

// 取数据,如果队列为空,就一直阻塞,直到有其他线程往队列中放数据
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 加锁,加可中断的锁
lock.lockInterruptibly();
try {
// 如果队列为空,就一直阻塞,直到被唤醒
while (count == 0)
notEmpty.await();
// 如果队列不为空,就从队列取数据
return dequeue();
} finally {
// 结束后,别忘了释放锁
lock.unlock();
}
} // 实际从队列取数据的方法
private E dequeue() {
// 获取数组
final Object[] items = this.items;
// takeIndex 表示本次取数据的位置,是上一次取数据时计算好的
E x = (E) items[takeIndex];
// 取完之后,就把队列该位置的元素删除
items[takeIndex] = null;
// ++takeIndex 计算下次取数据的位置
// 如果本次取数据的位置,正好是队尾,下次就从 0 开始取数据
if (++takeIndex == items.length)
takeIndex = 0;
// 元素数量减一
count--;
if (itrs != null)
itrs.elementDequeued();
// 唤醒被队列满所阻塞的线程
notFull.signal();
return x;
}

4.4 总结

  1. ArrayBlockingQueue基于数组实现的阻塞队列,创建队列时需指定容量大小,是有界队列。
  2. ArrayBlockingQueue底层采用循环队列的形式,保证数组位置可以重复使用。
  3. ArrayBlockingQueue存取都采用ReentrantLock加锁,保证线程安全,在多线程环境下也可以放心使用。
  4. 使用ArrayBlockingQueue的时候,预估好队列长度,保证生产者和消费者速率相匹配。

我是「一灯架构」,如果本文对你有帮助,欢迎各位小伙伴点赞、评论和关注,感谢各位老铁,我们下期见

不允许还有Java程序员不了解BlockingQueue阻塞队列的实现原理的更多相关文章

  1. [转载]一个标准java程序员的进阶过程

    第一阶段:Java程序员 技术名称 内                 容 说明 Java语法基础 基本语法.数组.类.继承.多态.抽象类.接口.object对象.常用类(Math\Arrarys\S ...

  2. Java程序员应该了解的10个面向对象设计原则

    面向对象设计原则: 是OOPS(Object-Oriented Programming System,面向对象的程序设计系统)编程的核心,但大多数Java程序员追逐像Singleton.Decorat ...

  3. To Java程序员:切勿用普通for循环遍历LinkedList

    ArrayList与LinkedList的普通for循环遍历 对于大部分Java程序员朋友们来说,可能平时使用得最多的List就是ArrayList,对于ArrayList的遍历,一般用如下写法: p ...

  4. Java 程序员们值得一看的好书推荐

    "学习的最好途径就是看书",这是我自己学习并且小有了一定的积累之后的第一体会.个人认为看书有两点好处: 能出版出来的书一定是经过反复的思考.雕琢和审核的,因此从专业性的角度来说,一 ...

  5. Java程序员应该掌握的10项技能

    这篇文章主要介绍了作为Java程序员应该掌握的10项技能,包括java的知识点与相关的技能,对于java的学习有不错的参考借鉴价值,需要的朋友可以参考下   1.语法:必须比较熟悉,在写代码的时候ID ...

  6. Java程序员岗位

    Java程序员岗位面试题有哪些?   1.面向对象的特征有哪些方面(1)抽象:抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面.抽象并不打算了解全部问题,而只是选择 ...

  7. Java程序员

    从生存.制胜.发展三个方面入手,为大家展示出程序员求职与工作的一幅3D全景图像.本书中既有在公司中的生存技巧,又有高手达人的进阶策略,既有求职攻略的按图索骥,又有入职后生产环境的破解揭秘. 书中浓缩了 ...

  8. Java程序员学习之路

    1. Java语言基础 谈到Java语 言基础学习的书籍,大家肯定会推荐Bruce Eckel的<Thinking in Java>.它是一本写的相当深刻的技术书籍,Java语言基础部分基 ...

  9. 分享下对JAVA程序员成长之路的总结<转>

    我也搞了几年JAVA了,由于一向懒惰,没有成为大牛,只是一普通程序猿,手痒来给新人分享下从新手成长为老鸟的已见.   首先初识语法的阶段,必须要学会怎么操作对象,操作if和for,操作list set ...

随机推荐

  1. 适合初学者的使用CNN的数字图像识别项目:Digit Recognizer with CNN for beginner

    准备工作 数据集介绍 数据文件 train.csv 和 test.csv 包含从零到九的手绘数字的灰度图像. 每张图像高 28 像素,宽 28 像素,总共 784 像素.每个像素都有一个与之关联的像素 ...

  2. 当我们谈论算法我们在谈论什么:由疫情核酸检测想到的分治算法(Divide-and-Conquer)

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_159 北京的疫情一波未平一波又起,由此看来,战"疫"将是一场旷日持久的战争,绝不能掉以轻心.轻易言胜.病毒随时 ...

  3. Spherical类定义和实现

    此类是一个全景摄像机视角,书上介绍了详细原理.直接给实现代码. 类声明: #pragma once #ifndef __SPHERICAL_HEADER__ #define __SPHERICAL_H ...

  4. Eclipse里项目名有红叉,但是项目里的每一个文件都没有红叉

    原因一:导入的文件被删除了. 解决方法:右击项目名,在弹出的菜单中,选择"Bulid Path"-->"configure build path"--&g ...

  5. Taurus.MVC 微服务框架 入门开发教程:项目部署:3、微服务应用程序版本升级:全站升级和局部模块升级。

    系列目录: 本系列分为项目集成.项目部署.架构演进三个方向,后续会根据情况调整文章目录. 本系列第一篇:Taurus.MVC V3.0.3 微服务开源框架发布:让.NET 架构在大并发的演进过程更简单 ...

  6. HTTP/3,它来了

    HTTP 3.0 是 HTTP 协议的第三个主要版本,前两个分别是 HTTP 1.0 和 HTTP 2.0 ,但其实 HTTP 1.1 我认为才是真正的 HTTP 1.0. 如果你对 HTTP 1.1 ...

  7. Word 分栏符怎么使用

    如果你使用栏来设置文档格式(如在某些新闻稿布局中),则文本将自动从一栏排列到另一栏.可以插入你自己的分栏符,以更好地控制文档格式. 单击"布局"选项卡--"页面设置&qu ...

  8. suse 11 sp3编译报错问题

    suse安装,默认是不安装gcc的,今天安装gcc之后,编译suse11 sp3,报错如下: scripts/basic/fixdep.c:106:23: fatal error: sys/types ...

  9. jQuery使用case记录

    添加元素/内容追加等 元素内: append() - 在被选元素的结尾插入内容 prepend() - 在被选元素的开头插入内容 元素外: after() - 在被选元素之后插入内容 before() ...

  10. 简单创建一个SpringCloud2021.0.3项目(四)

    目录 1. 项目说明 1. 版本 2. 用到组件 3. 功能 2. 上三篇教程 3. 日志处理 1. 创建日志公共模块 2. Eureka引入日志模块 4. 到此的功能代码 5. 注册中心换成naco ...