惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
SegmentFault 最新的问题
V
Visual Studio Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
量子位
月光博客
月光博客
阮一峰的网络日志
阮一峰的网络日志
T
Tailwind CSS Blog
GbyAI
GbyAI
爱范儿
爱范儿
Y
Y Combinator Blog
宝玉的分享
宝玉的分享
有赞技术团队
有赞技术团队
罗磊的独立博客
Recent Announcements
Recent Announcements
博客园 - 司徒正美
M
MIT News - Artificial intelligence
小众软件
小众软件
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
B
Blog RSS Feed
A
About on SuperTechFans
Hugging Face - Blog
Hugging Face - Blog
Apple Machine Learning Research
Apple Machine Learning Research
雷峰网
雷峰网

博客园 - 小飞龙(Jack)

redis消息队列 Redis实现消息队列 阻塞的队列 线程安全问题 单线程与多线程的区别 什么场景用到多线程 Java解决高并发秒杀商品 秒杀架构模型设计 大型网站如何防止崩溃,解决高并发带来的问题 序列化与反序列号 普通常用知识汇总 接口和抽象类的区别 详解C#break ,continue, return AJAX原理 比特(bit)与字节(byte)区别,站位比较 NVARCHAR 和VARCHAR区别和使用 C# 中的委托和事件 深拷贝与浅拷贝探析 String、StringBuffer与StringBuilder之间区别
BlockingQueue介绍
小飞龙(Jack) · 2023-07-26 · via 博客园 - 小飞龙(Jack)

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

1. BlockingQueue的应用场景

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

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

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

image

你可能联想到了消息队列(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 {
        

为了满足不同的使用场景,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;

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

4.2 put方法源码解析

image

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

// 放数据,如果队列已满,就一直阻塞,直到有其他线程从队列中取走数据
public void put(E e) throws InterruptedException {
    

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

像下面这样:

image

4.3 take方法源码

// 取数据,如果队列为空,就一直阻塞,直到有其他线程往队列中放数据
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
  	

4.4 总结

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