(接上文《源碼閱讀(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 order
itrs.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)》)