源碼閱讀(32):Java中線程安全的Queue、Deque結構——ArrayBlockingQueue(2)

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章