JDK数组阻塞队列源码深入剖析

前言

在前面一篇文章从零开始自己动手写阻塞队列当中我们仔细介绍了阻塞队列提供给我们的功能,以及他的实现原理,并且基于谈到的内容我们自己实现了一个低配版的数组阻塞队列。在这篇文章当中我们将仔细介绍JDK具体是如何实现数组阻塞队列的。

阻塞队列的功能

而在本篇文章所谈到的阻塞队列当中,是在并发的情况下使用的,上面所谈到的是队列是并发不安全的,但是阻塞队列在并发下情况是安全的。阻塞队列的主要的需求如下:

  • 队列基础的功能需要有,往队列当中放数据,从队列当中取数据。
  • 所有的队列操作都要是并发安全的。
  • 当队列满了之后再往队列当中放数据的时候,线程需要被挂起,当队列当中的数据被取出,让队列当中有空间的时候线程需要被唤醒。
  • 当队列空了之后再往队列当中取数据的时候,线程需要被挂起,当有线程往队列当中加入数据的时候被挂起的线程需要被唤醒。
  • 在我们实现的队列当中我们使用数组去存储数据,因此在构造函数当中需要提供数组的初始大小,设置用多大的数组。

上面就是数组阻塞队列给我们提供的最核心的功能,其中将线程挂起和唤醒就是阻塞队列的核心,挂起和唤醒体现了“阻塞”这一核心思想。

数组阻塞队列设计

阅读这部分内容你需要熟悉可重入锁ReentrantLock和条件变量Condition的使用。

数组的循环使用

因为我们是使用数组存储队列当中的数据,从下表为0的位置开始,当我们往队列当中加入一些数据之后,队列的情况可能如下,其中head表示队头,tail表示队尾。

在上图的基础之上我们在进行四次出队操作,结果如下:

在上面的状态下,我们继续加入8个数据,那么布局情况如下:



我们知道上图在加入数据的时候不仅将数组后半部分的空间使用完了,而且可以继续使用前半部分没有使用过的空间,也就是说在队列内部实现了一个循环使用的过程。

字段设计

在JDK当中数组阻塞队列的实现是ArrayBlockingQueue类,在他的内部是使用数组实现的,我们现在来看一下它的主要的字段,为了方便阅读将所有的解释说明都写在的注释当中:

    /** The queued items */
final Object[] items; // 这个就是具体存储数据的数组 /** items index for next take, poll, peek or remove */
int takeIndex; // 因为是队列 因此我们需要知道下一个出队的数据的下标 这个就是表示下一个将要出队的数据的下标 /** items index for next put, offer, or add */
int putIndex; // 我们同时也需要下一个入队的数据的下标 /** Number of elements in the queue */
int count; // 统计队列当中一共有多少个数据 /*
* Concurrency control uses the classic two-condition algorithm
* found in any textbook.
*/ /** Main lock guarding all access */
final ReentrantLock lock; // 因为阻塞队列是一种可以并发使用的数据结构 /** Condition for waiting takes */
private final Condition notEmpty; // 这个条件变量主要用于唤醒被 take 函数阻塞的线程 也就是从队列当中取数据的线程 /** Condition for waiting puts */
private final Condition notFull; // 这个条件变量主要用于唤醒被 put 函数阻塞的线程 也就是从队列当中放数据的线程

构造函数

构造函数的主要功能是申请指定大小的内存空间,并且对类的成员变量进行赋值操作。

public ArrayBlockingQueue(int capacity) {
// capacity 表示用与存储数据的数组的长度
this(capacity, false);
}
// fair 这个参数主要是用于说明 是否使用公平锁
// 如果为 true 表示使用公平锁 执行效率低 但是各个线程进入临界区的顺序是先来后到的顺序 更加公平
// 如果为 false 表示使用非公平锁 执行效率更高
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
// 对变量进行赋值操作
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}

put函数

这个函数是阻塞队列对核心的函数之一了,首先我们需要了解的是,如果一个线程调用了这个函数往队列当中加入数据,如果此时队列已经满了则线程需要被挂起,如果没有满则需要将数据加入到队列当中,也就是将数据存储到数组当中。注意还有一个很重要的一点是,当我们往队列当中加入一个数据之后需要发一个信号给其他被take函数阻塞的线程,因为这些线程在取数据的时候可能队列当中已经空了,因此需要将这些线程唤醒。

public void put(E e) throws InterruptedException {
checkNotNull(e); // 保证输入的数据不为 null 代码在下方
final ReentrantLock lock = this.lock;
// 进行加锁操作,因为下面是临界区
lock.lockInterruptibly();
try {
while (count == items.length) // 如果队列已经满了 也就是队列当中数据的个数 count == 数组的长度的话 就需要将线程挂起
notFull.await();
// 当队列当中有空间的之后将数据加入到队列当中 这个函数在下面仔细分析 代码在下方
enqueue(e);
} finally {
lock.unlock();
}
} private static void checkNotNull(Object v) {
if (v == null)
throw new NullPointerException();
} private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
// 进入这个函数的线程已经在 put 函数当中加上锁了 因此这里不需要加锁
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length) // 因为这个数据是循环使用的 因此可以回到下标为0的位置
// 因为队列当中的数据可以出队 因此下标为 0 的位置不存在数据可以使用
putIndex = 0;
count++;
// 在这里需要将一个被 take 函数阻塞的线程唤醒 如果调用这个方法的时候没有线程阻塞
// 那么调用这个方法相当于没有调用 如果有线程阻塞那么将会唤醒一个线程
notEmpty.signal();
}

注意:这里有一个地方非常容易被忽略,那就是在将线程挂起的时候使用的是while循环而不是if条件语句,代码:

final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}

这是因为,线程被唤醒之后并不会立马执行,因为线程在调用await方法的之后会释放锁,他想再次执行还需要再次获得锁,然后就在他获取锁之前的这段时间里面,可能其他的线程也会从数组当中放数据,因此这个线程执行的时候队列可能还是满的,因此需要再次判断,否则就会覆盖数据,像这种唤醒之后并没有满足线程执行条件的现象叫做虚假唤醒,因此大家在写程序的时候要格外注意,当需要将线程挂起或者唤醒的之后,最好考虑清楚,如果不确定可以使用while替代if,这样的话更加保险。

take函数

这个函数主要是从队列当中取数据,但是当队列为空的时候需要将调用这个方法的线程阻塞。当队列当中有数据的时候,就可以从队列当中取出数据,但是有一点很重要的就是当从队列当中取出数据之后,需要调用signal方法,用于唤醒被 put 函数阻塞的线程,因为从队列当中取出数据了,队列肯定已经不满了,因此可以唤醒被 put 函数阻塞的线程了。

public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 因为取数据的代码涉及到数据竞争 也就是说多个线程同时竞争 数组数据items 因此需要用锁保护起来
lock.lockInterruptibly();
try {
// 当 count == 0 说明队列当中没有数据
while (count == 0)
notEmpty.await();
// 当队列当中还有数据的时候可以将数据出队
return dequeue();
} finally {
lock.unlock();
}
} private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
// 取出数据
E x = (E) items[takeIndex];
items[takeIndex] = null; // 将对应的位置设置为 null 数据就可以被垃圾收集器回收了
if (++takeIndex == items.length)
takeIndex = 0;
count--;
// 迭代器也需要出队 如果不了
if (itrs != null)
itrs.elementDequeued();
// 调用 signal 函数 将被 put 函数阻塞的线程唤醒 如果调用这个方法的时候没有线程阻塞
// 那么调用这个方法相当于没有调用 如果有线程阻塞那么将会唤醒一个线程
notFull.signal();
return x;
}

同样的道理这里也需要使用while循环去进行阻塞,否则可能存在虚假唤醒,可能队列当中没有数据返回的数据为 null,而且会破坏队列的结构因为会涉及队列的两个端点的值的改变,也就是takeIndexputIndex的改变。

offer函数

这个函数的作用和put函数一样,只不过当队列满了的时候,这个函数返回false,加入数据成功之后这个函数返回true,下面的代码就比较简单了。

public boolean offer(E e) {
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();
}
}

add函数

这个函数和上面两个函数的意义也是一样的,只不过当队列满了之后这个函数会抛出异常。

public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}

poll函数

这个函数和take函数的作用差不多,但是这个函数不会阻塞,当队列当中没有数据的时候直接返回null,有数据的话返回数据。

public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}

总结

在本篇文章当中主要介绍了JDK内部是如何实现ArrayBlockingQueue的,如果你对锁和队列的使用有一定的了解本篇文章应该还是比较容易理解的。在实现ArrayBlockingQueue当中有以下需要注意的点:

  • put函数,如果在往队列当中加入数据的时候队列满了,则需要将线程挂起。在队列当中有空间之后,线程被唤醒继续执行,在往队列当中加入了数据之后,需要调用signal方法,唤醒被take函数阻塞的线程。
  • take函数,如果在往队列当中取出数据的时候队列空了,则需要将线程挂起。在队列当中有数据之后,线程被唤醒继续执行,在从队列当中取出数据之后,需要调用signal方法,唤醒被put函数阻塞的线程。
  • 在调用await函数的时候,需要小心虚假唤醒现象。

以上就是本篇文章的所有内容了,我是LeHung,我们下期再见!!!更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。

JDK数组阻塞队列源码深入剖析的更多相关文章

  1. 聊聊 JDK 非阻塞队列源码(CAS实现)

    正如上篇文章聊聊 JDK 阻塞队列源码(ReentrantLock实现)所说,队列在我们现实生活中队列随处可见,最经典的就是去银行办理业务,超市买东西排队等.今天楼主要讲的就是JDK中安全队列的另一种 ...

  2. 细说并发5:Java 阻塞队列源码分析(下)

    上一篇 细说并发4:Java 阻塞队列源码分析(上) 我们了解了 ArrayBlockingQueue, LinkedBlockingQueue 和 PriorityBlockingQueue,这篇文 ...

  3. 聊聊 JDK 阻塞队列源码(ReentrantLock实现)

    项目中用到了一个叫做 Disruptor 的队列,今天楼主并不是要介绍 Disruptor 而是想巩固一下基础扒一下 JDK 中的阻塞队列,听到队列相信大家对其并不陌生,在我们现实生活中队列随处可见, ...

  4. 细说并发4:Java 阻塞队列源码分析(上)

    上篇文章 趣谈并发3:线程池的使用与执行流程 中我们了解到,线程池中需要使用阻塞队列来保存待执行的任务.这篇文章我们来详细了解下 Java 中的阻塞队列究竟是什么. 读完你将了解: 什么是阻塞队列 七 ...

  5. ArrayDeque(JDK双端队列)源码深度剖析

    ArrayDeque(JDK双端队列)源码深度剖析 前言 在本篇文章当中主要跟大家介绍JDK给我们提供的一种用数组实现的双端队列,在之前的文章LinkedList源码剖析当中我们已经介绍了一种双端队列 ...

  6. FutureTask源码深度剖析

    FutureTask源码深度剖析 前言 在前面的文章自己动手写FutureTask当中我们已经仔细分析了FutureTask给我们提供的功能,并且深入分析了我们该如何实现它的功能,并且给出了使用Ree ...

  7. libevent源码深度剖析十

    libevent源码深度剖析十 ——支持I/O多路复用技术 张亮 Libevent的核心是事件驱动.同步非阻塞,为了达到这一目标,必须采用系统提供的I/O多路复用技术,而这些在Windows.Linu ...

  8. libevent源码深度剖析九

    libevent源码深度剖析九 ——集成定时器事件 张亮 现在再来详细分析libevent中I/O事件和Timer事件的集成,与Signal相比,Timer事件的集成会直观和简单很多.Libevent ...

  9. Redis 源码简洁剖析 03 - Dict Hash 基础

    Redis Hash 源码 Redis Hash 数据结构 Redis rehash 原理 为什么要 rehash? Redis dict 数据结构 Redis rehash 过程 什么时候触发 re ...

随机推荐

  1. JS倒计时(刷新页面不影响)的实现思路

    最近在做一个项目,用到了点击按钮实现倒计时,这个用js来实现很简单.但是遇到了一个问题 页面刷新后js重新加载导致 倒计时重新开始,或者直接初始化了 后来通过 cookie 保存来实现了js倒计时,关 ...

  2. 缓存&PWA实践

    缓存&PWA 实践 一.背景 从上一篇<前端动画实现与原理分析>,我们从 Performance 进行动画的性能分析,并根据 Performance 分析来优化动画.但,前端不仅仅 ...

  3. Java开发学习(二)----IOC、DI入门案例

    一.IOC入门案例 1.1 思路分析 (1)Spring是使用容器来管理bean对象的,那么管什么? 主要管理项目中所使用到的类对象,比如(Service和Dao) (2)如何将被管理的对象告知IOC ...

  4. 拭目以待!JNPF .NET将更新.NET 6技术,同时上线 3.4.1 版本

    2022年5月30日,福建引迈即将上线JNPF开发平台的.NET 6版本,在产品性能上做了深度优化,且极大的提升了工作效率,加强了对云服务的改进升级,全面提升用户的使用体验. JNPF是一个以PaaS ...

  5. Fiddler对安卓高版本进行抓包解决方案以及分析 进阶二

    今天是2021年的最后一天了,多分享一些干货吧!看过上一章节教程后会有同学疑惑,我也一步一个脚印的,跟着流程走也设置了代理以及安装了证书,有的同学会发现 为什么手机不能够连接网络了呢?细心一点的同学会 ...

  6. HtmlParse:一款超轻量级的HTML文件解析和爬取工具

    HtmlParse 是一款基于windwos平台的HTML文档解析工具,可快速构建DOM树,从而轻松实现网页元素的爬取工作.DOM树就是一个HTML文档的节点树,每个节点由:标签(Tag).属性(At ...

  7. 中国天气api接口xml,json

    http://m.weather.com.cn/data/101110101.html 大坑有木有??反应慢不说了,还老不更新!! 想贴段代码的,现在又打不 开了(貌似3月4号以后没更新过) ==== ...

  8. ESXI系列问题整理以及记录——使用SSH为设备打VIB驱动包,同时提供一种对于ESXI不兼容螃蟹网卡(Realtek 瑞昱)的问题解决思路

    对于ESXI不兼容螃蟹网卡的问题,这里建议购买一张博通的低端单口千兆网卡,先使用博通网卡完成系统部署,再按照下文方法添加螃蟹网卡的VIB驱动,最后拆除博通网卡. 螃蟹网卡VIB驱动包下载地址:http ...

  9. Java实用类(五) -Math类和指定范围的随机数

    1.Math类 java.lang.Math类提供了常用的数学运算方法和两个静态常量E(自然对数的底数) 和PI(圆周率) // 绝对值 System.out.println(Math.abs(-3. ...

  10. $.fn解析

    $.fn是指jquery的命名空间,加上fn上的方法及属性,会对jquery实例每一个有效. 如扩展$.fn.abc(),即$.fn.abc()是对jquery扩展了一个abc方法,那么后面你的每一个 ...