(接上文《源码阅读(31):Java中线程安全的Queue、Deque结构——ArrayBlockingQueue(1)》)

本篇内容我们专门分析ArrayBlockingQueue中迭代器的工作情况,ArrayBlockingQueue迭代器非常有阅读意义,是java集合框架中比较有代表性的结构之一。

2.3、ArrayBlockingQueue的迭代器

2.3.1、迭代器的使用和产生的问题

在进行ArrayBlockingQueue迭代器的使用讲解前,我们先来看看ArrayBlockingQueue迭代器的基本使用。

// ......
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(20);
// ......
queue.add("4");
queue.add("5");
queue.add("6");
// ......
boolean removed = queue.remove("5");
System.out.println("移除操作是否有效:" + removed);
// 新建一个迭代器
Iterator<String> itr = queue.iterator();
while(itr.hasNext()) {System.out.println(itr.next());
}
System.out.print("队列中还有元素吗:" + !queue.isEmpty());
// 这又是另一个新建的迭代器
itr = queue.iterator();
while(itr.hasNext()) {// 取得下一个数据String nextItem = itr.next();// 删除ArrayBlockingQueue中,最后一次使用next()方法获取的索引位上的数据itr.remove();System.out.println("//====被移除的元素是:" + nextItem);
}
System.out.print("队列中还有元素吗:" + !queue.isEmpty());
// ......

以下是这个代码片段的输出结果:

移除操作是否有效:true
1
2
3
4
6
7
队列中还有元素吗:true
//====被移除的元素是:1
//====被移除的元素是:2
//====被移除的元素是:3
//====被移除的元素是:4
//====被移除的元素是:6
//====被移除的元素是:7
队列中还有元素吗:false

这里要说明的是,虽然大部分资料(包括本文)都在试图给读者说明ArrayBlockingQueue是一种符合FIFO规则的有界阻塞队列,但在一些操作场景下,ArrayBlockingQueue也并不绝对遵循FIFO规则——我们可以在ArrayBlockingQueue非头部和尾部的某个索引位置移除一个元素,有以下几种方式:

1、调用ArrayBlockingQueue的remove(Object)方法,从队列的某个位置移除和入参对象“相等”的一个元素,这个方法是由java.util.AbstractCollection抽象类定义。使用方式如下所示:

// ......
// 移除指定的元素,无论它处于队列的哪一个索引位
boolean removed = queue.remove("5");
// ......

2、使用ArrayBlockingQueue的迭代器,并调用迭代器中的remove()方法。后者这个方法可以将移除ArrayBlockingQueue队列中通过本itr迭代器的next()方法最后一次获取到的索引位上的数据,如下所示:

// ......
while(itr.hasNext()) {String nextItem = itr.next();// 删除ArrayBlockingQueue中,最后一次使用next()方法获取的索引位上的数据itr.remove();
}
// ......

3、另外,有的读者会提到drainTo(Collection , int)方法,该方法将当前队列集合中指定数量的元素移入指定集合,并在当前队列集合中进行删除。这个方法实际上是从队列头(takeIndex)的索引位开始操作的,所以严格意义上还是有细微差异,但是该方法确实会对造成迭代器工作不准确。如下所示:

// ......
ArrayBlockingQueue<String> source = new ArrayBlockingQueue<>(20);
source.add("1");
source.add("2");
// ......
source.add("7");
HashSet<String> targetc = new HashSet<>();
source.drainTo(targetc , 3);
// ......

在多线程场景下,各迭代器可能由多个线程同时进行操作,这就导致以下几种可能性:

  • 某迭代器进行了ArrayBlockingQueue队列移除操作,但是另外的迭代器却并不知道,后者依然按照原来ArrayBlockingQueue队列中各索引位位的情况进行读写/遍历操作。

  • ArrayBlockingQueue队列本身发生了数据新增/移除操作,但是多有迭代器都不知道,后者依然按照依然按照原来ArrayBlockingQueue队列中各索引位位的情况进行读写/遍历操作。

加之ArrayBlockingQueue队列内部是一个可循环利用的环形数组,这就使得迭代器在工作时,只是利用ArrayBlockingQueue队列自身的状态情况,很难识别ArrayBlockingQueue队列中的数据是否发生了变化。例如,当以下示意图的情况发生时,我们能肯定ArrayBlockingQueue队列在两次next()方法执行的间隙没有发生变化吗?

如上图所示,在itr迭代器两次调用next()方法之间,另外的线程操作ArrayBlockingQueue队列进行了多次数据添加/移除操作,但由于ArrayBlockingQueue队列内部环形数组的原因,其takeIndex索引、putIndex索引、count数值均没有发生变化。但ArrayBlockingQueue队列中的实际数据已经全变了,takeindex已经在环形数组中“绕场一圈”。

2.3.2、迭代器工作原理概述

由于ArrayBlockingQueue队列的特殊结构,以及上述需要保证的各种特殊工作场景(各种多线程操作,多种数据移除操作),导致Itr迭代器比较复杂——复杂到本专题需要专门花1-2篇文章篇幅,对这个迭代器和其工作原理进行详细介绍。

2.3.2.1、迭代器组

ArrayBlockingQueue为了管理一个和多个迭代器,专门设立了一个Itrs迭代器组的概念,除了detached(独立/无效)工作模式下的迭代器外,ArrayBlockingQueue队列中目前所有正在被使用的迭代器都基于Itrs迭代器组构造成一个单向链表结构,列表中的每个节点使用“弱引用”方式进行对象引用。如下图所示:

迭代器和迭代器组的工作目标是,尽可能正确的完成ArrayBlockingQueue队列中所有数据的遍历操作,而不是,在数据出现遍历差异时,尽可能将迭代器设定独立/无效工作模式。当每次ArrayBlockingQueue发生“取数”操作时,当每次有新的迭代器创建时,Itrs迭代器组都要进行相关判定和维护,以保证所有迭代器的一致性,并对无效/无法维护的迭代器进行清理。

所谓“取数”操作是概指那些需要从ArrayBlockingQueue移除数据的操作,包括:所有需要调用ArrayBlockingQueue.dequeue()方法的操作(例如poll()、take()这些方法)、所有需要调用ArrayBlockingQueue.removeAt(int)方法的操作(例如remove(Object)这样的方法)、以及进行数据批量移除的操作(例如drainTo(Collection)、drainTo(Collection, int )这样的方法)。

detached(独立/无效)工作模式是指,在特定场景下创建的没有任何数据可以遍历的迭代器,或者已经完成所有数据遍历的迭代器。例如当ArrayBlockingQueue队列集合没有任何数据时创建的迭代器。这类迭代器不能遍历任何数据,也就不涉及到要保证遍历时索引位正确性的需求。所以这类“独立/无效”工作模式的迭代器无需加入到迭代器管理组进行管理。

以下是Itrs迭代器组中的重要属性定义,有了这些属性定义,我们就可以为所有在工作中的Itr迭代器扩展ArrayBlockingQueue队列所需的描述了:

// ......
class Itrs {/*** Node in a linked list of weak iterator references.* 这是Itrs迭代器组的一个Node节点定义*/private class Node extends WeakReference<Itr> {// next属性指向Itrs迭代器组单向链表中的下一个Node节点Node next;// 每一个Node节点都弱引用一个iterator迭代器(如上图所示)Node(Itr iterator, Node next) {super(iterator);this.next = next;}}/** * Incremented whenever takeIndex wraps around to 0 * 该属性非常重要,它记录takeIndex索引重新回到0号索引位的次数* 由此来描述takeIndex索引位的“圈数”*/int cycles;/** * Linked list of weak iterator references * 这是Itrs迭代器的第一个Node节点的,以便进行整个单向链表的构建、遍历和管理* */private Node head;/** * Used to expunge stale iterators * Itrs迭代器组在特定的场景下会进行Node单向链表的清理,该属性表示上次一清理到的Node节点* 以便在下一次清理时使用(不用回到head处重新遍历了)* */private Node sweeper;
}
// ......

2.3.2.2、为什么要使用“弱引用”来构建Itrs单向链表

在之前的文章中,我们已经介绍过Java中四种引用类型,以及每种类型的工作特点。被“弱引用”的对象在GC回收器进行可回收扫描时,若发现该对象只有“弱引用”可达时,就会将该对象进行回收。如下图所示:

弱引用的使用场景可以概括归纳为:在被监控对象被其引用者设置为null时,便于该对象相关监控设施的回收。如上图所示:A对象的强引用来自于C对象和D对象,读者可以理解为A、C、D三个对象都是用于处理业务逻辑的对象,而A对象的弱引用来自于B对象,后者引用A对象只是为了实时收集A对象的当前数值状态,以便周期性的写入日志系统

正常情况上来说,A、C、D对象都应该在业务逻辑完成后被GC回收,判定标准就是A、C、D对象引用的不可达,如下图所示:

也就是说一旦A对象引用不可达,就说明A对象可以被GC回收了,但这时如果B对象引用A也是强引用形式,就会导致A对象不能被GC回收器回收。这种情况下,我们就需要设定B对象对A对象的引用是一张弱引用,以便保证A对象在所有强引用都不可达时,能够被GC回收器回收。

以上的示例可以非常贴合的换成我们现在正在讨论的场景:A对象就是我们这里讨论的迭代器,B对象就是ArrayBlockingQueue队列集合中用于监管迭代器运行的Itrs迭代器组中的Node节点对象,C和D对象就是创建并使用迭代器的两个工作对象。

当C、D对象完成了迭代器使用后,将迭代器对象的引用置为null,就此断开和迭代器对象的引用关系(甚至C、D对象本身也不再可达),当GC进行内存清理时,就会将迭代器对象进行清理,而不会考虑Itrs迭代器组中的Node节点是否依然引用了这个迭代器对象。

2.3.2.3、Itr迭代器中的主要属性

Itrs迭代器组我们进行了简要的介绍,接着我们来节点Itr迭代器。要理解Itr迭代器的工作原理,我们就需要首先理解Itr迭代器的主要定义过程,如下代码片段所示:

// ......
private class ArrayBlockingQueue.Itr implements Iterator<E> {// ....../** Index to look for new nextItem; NONE at end */// 当前游标索引位private int cursor;/** Element to be returned by next call to next(); null if none */// 专门为支持hashNext方法和next方法配合所使用的属性,用于在调用next方法返回数据private E nextItem;/** Index of nextItem; NONE if none, REMOVED if removed elsewhere */// 专门为支持hashNext方法和next方法配合所使用的属性,记录调用next方法返回数据的索引位private int nextIndex;/** Last element returned; null if none or not detached. */// 最后一次(上一次)迭代器遍历操作时返回的元素private E lastItem;/** Index of lastItem, NONE if none, REMOVED if removed elsewhere */// 最后一次(上一次)迭代器遍历操作时返回的元素的索引位private int lastRet;/** Previous value of takeIndex, or DETACHED when detached */// 该变量表示本迭代器最后一次(上一次)从ArrayBlockingQueue队列中获取到的takeIndex索引位// 该属性还有一个重要的作用,用来表示当前迭代器是否是“独立”工作模式(或者迭代器是否失效)private int prevTakeIndex;/** Previous value of iters.cycles */// 最后一次(上一次)从ArrayBlockingQueue队列获取的takeIndex索引位回到0号索引位的次数// 这个值非常重要,是判定迭代器是否有效的重要依据private int prevCycles;/** Special index value indicating "not available" or "undefined" */private static final int NONE = -1;/*** Special index value indicating "removed elsewhere", that is,* removed by some operation other than a call to this.remove().*/// 该常量表示索引位所表示的值已经被remove()方法以外的操作移除private static final int REMOVED = -2;/** Special value for prevTakeIndex indicating "detached mode" */// 该常量值赋值到prevTakeIndex,以表示当前迭代器变成“独立”(无效)工作模式private static final int DETACHED = -3;// ......
}
// ......

特别注意以上三个常量NONE、REMOVED和DETACHED,这三个常量分别代表索引位的三种状态:

  • NONE:一般用来表示指定的索引位已完成任务或者不可用(主要用于Itr迭代器的lastRet索引、nextIndex索引);
  • REMOVED:一般用来表示指定的索引位上的元素已经被其它线程的操作移除(用于Itr迭代器的lastRet索引、nextIndex索引)
  • DETACHED:一般标识在prevTakeIndex变量上,表示当前迭代器为“独立/无效”工作模式(主要用于Itr迭代器的prevTakeIndex索引)。

另外通过以上Itr迭代器属性定义的描述可知,为了保证迭代器操作的正确性,Itr迭代器除了记录当前游标位置外,还完整记录了迭代器开始遍历的索引位置和next()方法将要返回的下一个元素(包括索引位置和对象)。

以上保证迭代器正确工作的一个典型场景就是迭代器的next()方法和hasNext()方法进行配合使用时——迭代器可以保证调用hasNext()方法返回true时,next()方法一定不会返回null。

2.3.2.4、Itr迭代器的实例化过程

以下代码片段描述了Itr迭代器的实例化过程:

// ......
Itr() {// assert lock.getHoldCount() == 0;lastRet = NONE;final ReentrantLock lock = ArrayBlockingQueue.this.lock;// 进行迭代器的初始化,也需要获取操作锁lock.lock();try {// 如果当前条件成立,说明这时ArrayBlockingQueue队列集合没有任何元素// 这时将当前迭代器作为独立模式进行创建。if (count == 0) {// assert itrs == null;// 当前游标索引无意义cursor = NONE;// 下一迭代索引位无意义nextIndex = NONE;// 使用该变量,标识当前迭代器独立工作,无需注册到Itrs迭代器组中prevTakeIndex = DETACHED;} else {// 将当前ArrayBlockingQueue队列集合的takeIndex值(下一个取数索引位)// 记录到prevTakeIndex变量中,作为当前迭代器开始遍历的索引位置final int takeIndex = ArrayBlockingQueue.this.takeIndex;prevTakeIndex = takeIndex;// 取出当前开始遍历的索引位上的数据,记录到nextItem变量中(nextIndex值也同时设定),作为将要调用的next()方法的返回值nextItem = itemAt(nextIndex = takeIndex);// 确定下一个游标位(incCursor(int)方法很重要,具体过程请参见该方法上的注释)// 迭代器初始化时,第一个游标位是takeIndex索引位的下一个索引位// 这是因为遍历起始索引位已经记录在了prevTakeIndex变量中cursor = incCursor(takeIndex);// 通过以上过程,迭代器的初始化过程基本完成,现在将这个迭代器对象注册到Itrs迭代器组中// 如果Itrs迭代器组还没有初始化,则进行Itrs组的初始化,并将当前迭代器对象作为Itrs迭代器组的第一个Node节点if (itrs == null) {itrs = new Itrs(this);}// 其它情况则将当前迭代器注册到Itrs迭代器组中,并清理Itrs迭代器组中过期/无效的迭代器节点。else {itrs.register(this); // in this orderitrs.doSomeSweeping(false);}// Itrs迭代器组中最重要的一个数值就是当前ArrayBlockingQueue队列集合takeIndex变量通过循环数组0号索引位的次数// 这个次数记录在Itrs迭代器组的cycles变量中,前者将在这里被赋值给迭代器的prevCycles变量prevCycles = itrs.cycles;// assert takeIndex >= 0;// assert prevTakeIndex == takeIndex;// assert nextIndex >= 0;// assert nextItem != null;}} finally {lock.unlock();}
}// 该私有方法根据ArrayBlockingQueue队列集合的固定长度和状态
// 确定下一个游标索引值。
private int incCursor(int index) {// 有几种情况:// a、如果下一个索引值等于当前队列容量,说明当前遍历的位置跨过了环形数组的0号索引位,这时设置下一游标位置为0// b、如果下一个索引值等于ArrayBlockingQueue队列putIndex索引值,说明已经没有能遍历的数据了,这时设置下一游标位置为NONE// c、其它情况下,index简单+1,就是下一个游标位置// assert lock.getHoldCount() == 1;if (++index == items.length)index = 0;if (index == putIndex)index = NONE;return index;
}
// ......

以上代码片段同样使用了非常详细的注释进行说明,并且可以通过以下示意图进行描述(注意,以下示意图只描述了count != 0的情况,而count == 0的情况很简单,这里就不再进行示意图的描述了):

上文中我们创建了一个新的Itr迭代器,由于这时ArrayBlockingQueue队列的中存在数据,所以新创建的Itr迭代器就不是“独立/无效”工作模式,而是需要加入到Itrs迭代器组中进行管理的迭代器。在创建过程中,迭代器的prevTakeIndex属性、nextIndex属性将被赋值为当前ArrayBlockingQueue队列takeIndex属性的值,其nextItem属性将会引用takeIndex索引位上的数据对象,其cursor属性将会指向takeIndex索引位的下一个索引位(incCursor(int)方法将负责修正索引值)。

============
(接下文《源码阅读(33):Java中线程安全的Queue、Deque结构——ArrayBlockingQueue(3)》)

源码阅读(32):Java中线程安全的Queue、Deque结构——ArrayBlockingQueue(2)相关推荐

  1. java中arraycopy的用法_[jdk源码阅读系列]Java中System.arraycopy()的用法

    本文转载,原文链接: 3分钟了解Java中System.arraycopy的用法 - 伊万夫斯基 - 博客园  https://www.cnblogs.com/benjieqiang/p/114288 ...

  2. 源码阅读(34):Java中线程安全的Queue、Deque结构——ArrayBlockingQueue(4)

    (接上文<源码阅读(33):Java中线程安全的Queue.Deque结构--ArrayBlockingQueue(3)>) 2.3.3.3.forEachRemaining() 方法 f ...

  3. 【源码阅读】Java集合之一 - ArrayList源码深度解读

    Java 源码阅读的第一步是Collection框架源码,这也是面试基础中的基础: 针对Collection的源码阅读写一个系列的文章,从ArrayList开始第一篇. ---@pdai JDK版本 ...

  4. mysql thread conn_MySQL源码阅读2-连接与线程管理

    本篇是第二篇,MySQL初始化完成之后,便进入一个死循环中,接受客户端请求,并完成客户端的命令(如果在window下启动多个listener,则分别启动线程监听).该篇介绍MySQL服务中的连接与线程 ...

  5. 通过跟踪源码证明在Java中通过执行Start()方法创建线程

    /**** 线程创建跟踪*/ public class ThreadCreate {public static void main(String[] args) {new Thread(()-> ...

  6. 源码阅读之Java栈的实现

    0x00 栈 栈是 Last-In-First-Out (后进先出)的线性表.对栈的操作主要有两个:入栈(push)和出栈(pop).因此它也是一种操作受限的线性表.尽管如此,它在计算机中应用非常广泛 ...

  7. 【一起读源码】1. Java 中元组 Tuple

    1.1 问题描述 使用 Java 做数据分析.机器学习的时候,常常需要对批量的数据进行处理,如果需要处理的数据的维度不超过10时,可以考虑使用 org.javatuples 提供的 Tuple 类工具 ...

  8. Tomcat源码阅读---ServletContext.java(小白写作,持续更新)

    everybody,想我没,刚考完操作系统,我胡汉三又回来啦!!!掌声!!!鲜花!!!接着奏乐,接着舞!!!来个大的!不要说我不够意思,这个ServletContext读完的确是让我对于web程序有了 ...

  9. 数据结构中缀表达式转后缀表达式与后缀表达式的求值实训报告_动图+源码,演示 Java 中常用数据结构执行过程及原理...

    程序员的成长之路互联网/程序员/成长/职场 关注 阅读本文大概需要 3.7 分钟. 作者:大道方圆cnblogs.com/xdecode/p/9321848.html 最近在整理数据结构方面的知识, ...

最新文章

  1. SAP MM 并非奇怪现象之MB5B报表里期初库存余额或者期末库存余额为负数?
  2. Myisamchk小工具使用手册
  3. Java Virtual Machine Stacks(虚拟机栈)
  4. 启用密码管理之前创建的用户连接Oracle报ORA-28002处理一则
  5. CCNP精粹系列之十八--路由映射实战二,博主推荐文章
  6. 要来吗,不错的WPF技术交流群!(大批干货今日自取)
  7. azkaban mysql参数_azkaban参数详解
  8. Linux 服务器远程控制三剑客Telnet、SSH 和 VNC 之 VNC
  9. MapReduce词频统计
  10. 模拟Windows任务管理器CPU使用率的动态折线图-农夫山泉
  11. Stata+R: 一文读懂中介效应分析
  12. mdt 计算机名_MDT通过PowerShell脚本自定义变量(自定义计算机名)
  13. 服务器显示504,帮您解决win7系统访问nginx服务器提示504 Gateway Time-out错误的修复技巧...
  14. 360度全景视频后视镜
  15. appcan外部网页css,Appcan开发之页面布局与CSS排版
  16. android好用的文件管理器,安卓哪种文件管理器好用 三款文件管理器横向评测
  17. 清华王牌专业建系70年,撑起中国半导体半壁江山,王兴李健都是系友
  18. 微信公众平台学习笔记
  19. python如何求每一行的均值_计算每X行数的平均值
  20. yxc_第一章 基础算法(三)_区间合并

热门文章

  1. MarkDown 自动生成目录方式
  2. 实战恢复cisco 2950交换机的IOS
  3. 低代码开发物联网app(2)-Power Apps 连接Azure SQL Database 读取物联网设备清单
  4. 常用的数学古希腊字母 读法
  5. 阿里天池金融风控baseline
  6. Android 屏幕录屏,然后转为GIF图
  7. Spark---JVM调优之调节executor离堆内存与连接等待时长
  8. SKB buff 数据结构
  9. UESTC 1646 穷且益坚, 不坠青云之志。
  10. 小学计算机课重要,浅谈信息技术课在小学教育教学中的重要性