文章目录
在看到 ArrayBlockingQueue 迭代器时感觉比之前看到的迭代器实现都要复杂,所有就专门提出来说一说。
ArrayBlockingQueue 迭代器与 ConcurrentHashMap 迭代器类似都是一种“弱一致性”迭代器,在遍历数组时修改数组并不会抛出 ConcurrentModificationException 异常。
为什么 ArrayBlockingQueue 迭代器复杂呢?
ArrayBlockingQueue 是由数组实现的有界队列,难就难在它是有界队列并且还要保证元素的顺序。使用数组实现有界队列,就只能使用记录队头、队尾索引的方式来实现一个循环队列。
在 ArrayBlockingQueue 中使用takeIndex
记录队头索引、putIndex
记录队尾索引用来操作队列。
/** items index for next take, poll, peek or remove */
int takeIndex;
/** items index for next put, offer, or add */
int putIndex;
迭代器中需要使用到takeIndex
,因为从队头向队尾遍历才能最大程度的保证遍历的一致性(可以遍历到创建迭代器之后添加到队列中的元素)。
提出几个 ArrayBlockingQueue 迭代器的问题用于下面代码分析时进行思考
- 怎样记录迭代器的遍历状态
- 如何判断迭代器当前状态是否有效
- 在迭代器创建完成之后并未立即使用,在多次入、出队操作或删除元素之后,迭代器会做如何处理
- 如果迭代器没有失效会怎么处理
- 如果迭代器失效了会怎么处理
Itrs
在 ArrayBlockingQueue 中使用 Itrs 维护这一个 Itr 链表,用于在一个队列下的多个 Itr 迭代器中共享队列元素,保证多个迭代器中的元素数据的一致性。
虽然 Itrs 这个设计增加了维护上的复杂性,但是为了保证迭代器在删除元素时,各个迭代器中能够保持一致,这个 Itrs 的设计时有必要的。Itrs 通过
- 跟踪 takeIndex 循环到 0 的次数。提供 takeIndexWrapped 方法,当 takeIndex 循环到 0 时,清除过期迭代。
- 提供 removedAt,通知所有的迭代器执行 removedAt 来保证所有的 Itr 迭代器数据保持一致。
以上的两项操作应当能够保证 Itr 迭代器间的一致性,但是增加了许多其他的操作来维护这些 Itr 迭代器。Itrs 通过一个链表和弱引用来维护 Itr 迭代器,并通过一下三种方式清空 Itr 迭代器:
- 当创建 Itr 迭代器时,检查链表中的 Itr 迭代器是否过期。
- 当 takeIndex 循环到 0 时,检查超过一次循环,但是从未被使用的迭代器。
- 如果队列被清空,那么所有的 Itr 迭代器都会被通知数据作废。
所以为了保证正确性,removedAt、shutdown 和 takeIndexWrapped 方法都做检查 Itr 迭代器是否过期的操作。如果元素都过期,GC 。如果决定迭代器作废或者迭代器通知自己过期,那么这些过期的元素会被清除。这个操作不需要做额外的其他操作就可完成。
Itr
当调用 iterator()
方法时,创建迭代器对象 Itr
public Iterator<E> iterator() {
return new Itr();
}
1.重要变量
这些变量记录着迭代器的遍历状态非常重要,在每次调用next()
都会去修正这些变量以维护迭代器的遍历状态。
/** Index to look for new nextItem; NONE at end */
private int cursor;
/** Element to be returned by next call to next(); null if none */
private E nextItem;
/** Index of nextItem; NONE if none, REMOVED if removed elsewhere */
private int nextIndex;
nextItem
:调用next()
方法的返回值
nextIndex
:nextItem
对象的索引
cursor
:nextIndex
的下一个位置的索引
在不断调用next()
方法获取元素的过程中,这三个变量以两个在前一个在后向后移动着。
/** 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;
lastItem
:上一次返回的元素
lastRet
:lastItem
的索引
需要注意:lastRet
、nextIndex
、cursor
这三个变量是判断迭代器是否失效的主要依据(满足 cursor < 0 && nextIndex < 0 && lastRet < 0 时代表迭代器失效
)
/** Previous value of takeIndex, or DETACHED when detached */
private int prevTakeIndex;
/** Previous value of iters.cycles */
private int prevCycles;
prevTakeIndex
:代表本次遍历开始的索引,每此 next 都会进行修正
prevCycles
:Itrs 管理 Itr 链,它里面有个变量 cycles 记录 takeIndex 回到 0 位置的次数,迭代器的 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().
*/
private static final int REMOVED = -2;
/** Special value for prevTakeIndex indicating "detached mode" */
private static final int DETACHED = -3;
DETACHED
:专门用于prevTakeIndex
,isDetached
方法通过其来判断迭代器状态是否失效
NONE
:用于三个下标变量:cursor
,nextIndex
,lastRet
;这三个下标变量用于迭代功能的实现。表明该位置数据不可用或未定义
REMOVED
:用于lastRet
与 nextIndex
。表明数据过时或被删除
2. 构造方法
在构造方法中对迭代器中的变量进行初始化
Itr() {
// assert lock.getHoldCount() == 0;
lastRet = NONE;
final ReentrantLock lock = ArrayBlockingQueue.this.lock;
// 在创建迭代器过程加锁,防止队列改变导致初始化错误
lock.lock();
try {
// 如果队列为空
if (count == 0) {
// assert itrs == null;
// 迭代器为DETACHED模式 - 失效状态
cursor = NONE;
nextIndex = NONE;
prevTakeIndex = DETACHED;
} else {
final int takeIndex = ArrayBlockingQueue.this.takeIndex;
// 初始化遍历起始索引prevTakeIndex为队头索引takeIndex
prevTakeIndex = takeIndex;
// 下一个遍历元素nextItem为队头元素itemAt(takeIndex)
// nextIndex为nextItem的索引
nextItem = itemAt(nextIndex = takeIndex);
// 获取nextIndex下一个元素的索引
cursor = incCursor(takeIndex);
// 如果itrs为null,就初始化;
if (itrs == null) {
itrs = new Itrs(this);
}
// 否则将当前迭代器注册到itrs中,并清理失效迭代器
else {
itrs.register(this); // in this order
itrs.doSomeSweeping(false);
}
// 当前迭代器记录最新的轮数
prevCycles = itrs.cycles;
// assert takeIndex >= 0;
// assert prevTakeIndex == takeIndex;
// assert nextIndex >= 0;
// assert nextItem != null;
}
} finally {
lock.unlock();
}
}
下面用一张存在元素的队列的示意图来展示一下初始过程各变量的状态:
关于 cursor 的增加操作:
private int incCursor(int index) {
// assert lock.getHoldCount() == 1;
if (++index == items.length)
index = 0;
// index == putIndex:标志着迭代结束
if (index == putIndex)
index = NONE;
return index;
}
当遍历到 putIndex ,代表数据遍历结束,应该终止迭代,将 cursor 置为 NONE 标识,cursor 置为 NONE 后会引起迭代器的终止,逻辑在 next 与 hasNext 方法中。
3. doSomeSweeping - 清除迭代器链
doSomeSweeping
方法不是在调用之后并非一次将整个链表探测一遍,而是根据传入参数boolean tryHarder
来选择探测范围是4或16,若在范围内未探测到无效迭代器则结束,若是探测到则扩大探测范围,将范围恢复为16,继续往下探测。
Itrs
中的三个相关变量
//记录上次探测的结束位置节点,下次从此开始往后探测。
private Node sweeper = null;
// 探测范围
private static final int SHORT_SWEEP_PROBES = 4;
private static final int LONG_SWEEP_PROBES = 16;
doSomeSweeping
将清除迭代器链中无效的迭代器结点:
- 结点持有的
Itr
对象为空,说明被GC回收,说明使用线程完成迭代 - 迭代器
Itr
为 DETACHED 模式,对于迭代结束或数据过时的迭代器会被置于 DETACHED 模式
void doSomeSweeping(boolean tryHarder) {
// assert lock.getHoldCount() == 1;
// assert head != null;
// tryHarder 为true则 probes 为 16,否则为 4
// probes 代表本次的探测长度
int probes = tryHarder ? LONG_SWEEP_PROBES : SHORT_SWEEP_PROBES;
// o 代表 p 的前一个节点,用于链表中节点的删除
Node o, p;
// 它代表了一次探测中到达的最后一个节点
final Node sweeper = this.sweeper;
/**
* passedGo:限制一次遍历
* 若从头开始遍历,passedGo设为true,
* 如果当前结点为null且probes>0可以直接break
* 若从中开始遍历,passedGo设为false,
* 如果当前结点为null且probes>0就会转回到头部继续遍历