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

(接上文《源碼閱讀(33):Java中線程安全的Queue、Deque結構——ArrayBlockingQueue(3)》)

2.3.3.3、forEachRemaining() 方法

forEachRemaining(Consumer<? super E> action) 方法是JDK 1.8+之後的版本新增的一個方法,是java.util.Iterator接口中定義的一個新方法,該方法類似於java.lang.Iterable接口定義的forEach(Consumer<? super T> action)方法。但是兩者是有區別的:

  • forEach(Consumer<? super T> action)方法在java.lang.Iterable接口中被定義,表示一個可迭代的java Collection Framework具體集合類(實際上java.lang.Iterable接口比java.util.Collection接口層次更低)。調用者對具體集合類的forEach方法調用多少次,後者就會執行多少次
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(20);
queue.add("1");
// ......
queue.add("6");
queue.add("7");
// 第一次執行forEach,並且會執行
queue.forEach(item -> {
  // ......
});
// 第二次執行forEach,並且會執行
queue.forEach(item -> {
  // ......
});
  • forEachRemaining(Consumer<? super E> action) 方法在java.util.Iterator接口中被定義,後者表示一個具體迭代器的實現。無論調用者對具體迭代的的forEachRemaining(Consumer<? super E> action) 方法調用多少次,後者只會執行一次
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(20);
queue.add("1");
// ......
queue.add("7");
// 創建一個迭代器
Iterator<String> itr = queue.iterator();
// 第一次執行forEachRemaining,並且會執行
itr.forEachRemaining(item -> {
  // ......
});
// 第二次執行forEachRemaining,但是會執行
itr.forEachRemaining(item -> {
  // ......
});

究其原因,是因爲forEachRemaining(Consumer<? super E> action) 方法默認基於迭代器的hasNext()方法和next()方法配合進行工作,在迭代器對象完成所有的數據的遍歷後,第二次調用同一個迭代器對象的forEachRemaining(Consumer<? super E> action) 方法,當然就不會執行了。以下是forEachRemaining方法的默認實現:

public interface Iterator<E> {
  // ......
  default void forEachRemaining(Consumer<? super E> action) {
    // Consumer過程必須定義,否則就拋出異常
    Objects.requireNonNull(action);
    // 這裏就是hasNext()方法和next()方法的配合工作
    while (hasNext()) {
      action.accept(next());
    } 
  } 
  // ......
}

由於ArrayBlockingQueue內部的循環數組結構和多線程場景下的工作要求,所以ArrayBlockingQueue隊列的迭代器中,對forEachRemaining方法的定義進行了調整,如下所示:

private class Itr implements Iterator<E> {
  // ......
  public void forEachRemaining(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    final ReentrantLock lock = ArrayBlockingQueue.this.lock;
    lock.lock();
    try {
      final E e = nextItem;
      if (e == null) return;
      if (!isDetached()) {
        incorporateDequeues();
      }
	  // 以上代碼片段是操作方法獲取到了ArrayBlockingQueue隊列的操作權後的規範化處理過程
	  // 這裏就不再進行贅述。我們主要從action.accept(e);這句代碼開始介紹
	  // 該語句將上次next()方法記錄的nextItem數據輸出給下一消費者(Consumer)
      action.accept(e);
      // 這裏還要再進行一次是否過期的判定,因爲incorporateDequeues()方法運行後,當前Itr迭代器可能已經無法取數
      // 具體的場景可參見上一篇文章的描述
      if (isDetached() || cursor < 0) {
        return;
      }
      final Object[] items = ArrayBlockingQueue.this.items;
      // 接着基於當前有效的cursor遊標位置,開始對ArrayBlockingQueue隊列中還沒有遍歷(且可遍歷)的數據進行遍歷
      for (int i = cursor, end = putIndex, to = (i < end) ? end : items.length; ; i = 0, to = end) {
        for (; i < to; i++) {
          action.accept(itemAt(items, i));
        }
        if (to == end) break;
      }
    } finally {
      // 當完成所有數據的遍歷後(無論成功還是失敗)
      // 將當前迭代器設置爲“獨立/無效”工作模式
      cursor = nextIndex = lastRet = NONE;
      nextItem = lastItem = null;
      detach();
      lock.unlock();
    }
  }
  // ......
}

2.3.4、Itrs迭代器組的清理過程

本小節我們來詳細講解一下Itrs迭代器組的清理過程。上文已經提到,ArrayBlockingQueue隊列集合中所有的迭代器都在Itrs迭代器組中進行管理,這些迭代器將在Itrs迭代器組中以單向鏈表的方式進行排列。所以ArrayBlockingQueue隊列需要在特定的場景下,對已經失效、甚至已經被垃圾回收的迭代器管理節點進行清理。

例如,當ArrayBlockingQueue隊列有新的迭代器被創建時(併爲非獨立/無效工作模式),Itrs迭代器組就會嘗試清理那些無效的迭代器,其工作邏輯主要由Itrs.doSomeSweeping(boolean)方法進行實現,代碼片段如下所示:

/**
 * 該方法負責對迭代器管理組Itrs進行清理。如果清理過程中發現了某個迭代器管理節點Itrs.Node
 * 需要被清理,則掃除過程會更努力的打掃後續的節點——“小掃除”變“大掃除”,當前掃除次數也會被重置
 * @param tryHarder 該方法採用“大掃除”還是“小掃除”方式進行清理。爲true的時候,表示使用“大掃除”模式
 */
void doSomeSweeping(boolean tryHarder) {
  // assert lock.isHeldByCurrentThread();
  // assert head != null;
  // 小掃除只會對單向鏈表清理4次,大掃除會至少清理16次
  int probes = tryHarder ? LONG_SWEEP_PROBES : SHORT_SWEEP_PROBES;
  Node o, p;
  // 從這個變量的單詞就可以知道,這是一臺掃除車
  // 這臺掃除車,將順着迭代器組中的單向隊列“向前開”
  final Node sweeper = this.sweeper;
  // 這是一個標記,最直白的理解就是“掃除車”是否開車
  boolean passedGo; 
  
  // sweeper(垃圾車)爲null的情況,主要的場景是Itrs迭代器組中沒有迭代器對象
  // 也可能是上次的清理操作已經將單向鏈表中所有的Node節點掃除完畢
  if (sweeper == null) {
    // 從當前單向鏈表的第一個Node節點開始,順着鏈表向後打掃
    o = null;
    p = head;
    passedGo = true;
  } else {
    // 從上次掃除結束的Node節點開始,繼續順着單向鏈表向後打掃
    o = sweeper;
    p = o.next;
    passedGo = false;
  }
  
  // ============ 以上過程決定“掃除車”從哪個地方開始打掃。以下就開始進行具體的打掃了。
  // 首先根據之前已經確認的掃除次數,決定是“大掃除”還是“小掃除”,16次清掃(循環)還是4次清掃(循環)
  // 注意,如果在清掃過程中,發現已經被回收或者已經“無效”的迭代器對象,則“小掃除”會變成“大掃除”
  for (; probes > 0; probes--) {
    // 如果條件成立,說明當前迭代器組Itrs集合中沒有任何迭代器對象,不需要進行掃除
    if (p == null) {
      if (passedGo) {
        break;
      }
      // 否則當p(開始掃除的Itrs.Node位置)爲null時,就從單向鏈表的頭節點,開始掃除
      o = null;
      p = head;
      passedGo = true;
    }
     
    // ==========   每一次掃除處理,都會做以下操作:
    // 取得當前這個Itrs.Node位置上所關聯的迭代器(注意這裏是一個弱引用)
    final Itr it = p.get();
    // 取得當前Itrs.Node位置的下一個Itrs.Node位置
    final Node next = p.next;
    // 如果條件成立,說明當前Itrs.Node需要被清理
    // 那麼就是用if塊中的代碼,進行清理,並且如果發現了這種場景,掃除車就會努力的進行後續的清理
    // 這就是該方法在最開始處提到的“更加努力的做後續清理”。否則,這個節點就不需要被清理
    if (it == null || it.isDetached()) {
      // found a discarded/exhausted iterator
      // 更加努力的做後續清理,“小掃除”會變成“大掃除”,且掃除次數會被重置
      probes = LONG_SWEEP_PROBES; // "try harder"
      // unlink p
      // 在it.isDetached()成立的場景下,主動清理這個弱引用
      p.clear();
      // 斷開p節點和單向鏈表後續節點的連接關係
      p.next = null;
      // 如果條件成立,則說明當前清掃的節點是單向鏈表的頭節點
      // 那麼需要重新設定頭節點爲當前被清掃節點的後續結點。
      // 如果都沒有後續結點了,那麼說明迭代器管理組中就沒有任何節點了,也就沒有必要繼續清掃下去(return退出)
      if (o == null) {
        head = next;
        if (next == null) {
          // We've run out of iterators to track; retire
          itrs = null;
          return;
        }
      }
      // 其它場景下,則通過該語句將當前被清掃的p節點徹底斷開和單向鏈表各節點的關係
      else {
        o.next = next;
      }
    } else {
      o = p;
    }
    p = next;
  }
  
  // 掃除結束後,根據最後p節點的引用情況,決定掃除車是停留在掃除結束節點上
  // 還是設置爲null。核心思路是,如果當前單向鏈表的所有Itrs.Node都掃除了一次,則掃除車沒有存在的必要了
  // 否則讓掃除車停留在掃除結束的位置上,以便下一次清掃請求被觸發時,繼續向後進行打掃
  this.sweeper = (p == null) ? null : o;
}

通過以上方法的詳細解讀,我們知道了迭代器管理組Itrs對其中迭代器對象的清理,主要包括以下關鍵點:

  • 首先根據當前sweeper掃除車的狀態,決定本次掃除的開始位置——在單向鏈表中掃除的開始位置
  • 在確認完掃除開始位置後,再依次進行掃除。掃除模式分爲“小掃除”和“大掃除”。
  • “小掃除”的定義是從掃除開始的位置,向後掃描最多4個節點。“小掃除”的目的在於節約操作步驟的同時,校驗鏈表中的迭代器部分正確。
  • “大掃除”的定義是從掃除開始的位置,至少向後掃描16個節點。“大掃除”的目的是保證鏈表中依然有效的迭代器的管理準確性。
  • “小掃除”模式可以轉變爲“大掃除”模式,既是“小掃除”過程中,發現其中一個Itrs.Node節點已經無效(爲null或者isDetached()爲true)

可以用下圖來表示doSomeSweeping(boolean)方法的主要清掃場景:

在這裏插入圖片描述

3、ArrayBlockingQueue隊列的主要構造函數

ArrayBlockingQueue隊列中一共有三個構造函數,如下所示:

public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
  // ......
  /**
   * 該構造函數給定一個capacity大小,以便設定ArrayBlockingQueue隊列中環形數組的最大容量
   * (也就是)ArrayBlockingQueue隊列的最大容量
   * 注意:如果capacity < 1,則會拋出異常
   */
  public ArrayBlockingQueue(int capacity) {
    this(capacity, false);
  }

  /**
   * 該構造函數給定兩個值,進行ArrayBlockingQueue隊列的實例化
   * @param capacity 當前ArrayBlockingQueue隊列的最大容量
   * @param fair 是否啓用公平鎖方式,默認情況下不啓用
   * @throws IllegalArgumentException if {@code capacity < 1}
   */
  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();
  }

  /**
   * 該構造函數給定三個值,進行ArrayBlockingQueue隊列的實例化
   * @param capacity 當前ArrayBlockingQueue隊列的最大容量
   * @param fair 是否啓用公平鎖方式,默認情況下不啓用
   * @param c 這是一個外部集合,這個集合不能爲null否則要報錯。
   * 這些集合中的數據將會按照特定的順序被複制到ArrayBlockingQueue隊列中
   */
  public ArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c) {
    this(capacity, fair);
    final ReentrantLock lock = this.lock;
    lock.lock(); // Lock only for visibility, not mutual exclusion
    try {
      final Object[] items = this.items;
      int i = 0;
      try {
        for (E e : c)
          items[i++] = Objects.requireNonNull(e);
      } catch (ArrayIndexOutOfBoundsException ex) {
        throw new IllegalArgumentException();
      }
      count = i;
      putIndex = (i == capacity) ? 0 : i;
    } finally {
      lock.unlock();
    }
  }
  // ......
}

ArrayBlockingQueue隊列自身的方法,相較於其下的Itr迭代器和Itrs迭代器分組而言,就要簡單許多了。例如其三個構造函數,不需要進行逐行註釋說明,讀者就能看懂其中的意義了。

這裏只需要特別注意的是構造函數中創建的兩個condition對象,關於condition對象的詳細介紹已經在講解AQS的章節中進行了詳細說明,這裏notEmpty對象負責在ArrayBlockingQueue隊列至少有一個數據的場景下,通知可能處於阻塞狀態的消費者線程結束阻塞狀態;notFull對象作用正好相反,它負責在ArrayBlockingQueue隊列至少有一個空餘的索引位可以放入新的數據時,通知可能處於阻塞狀態的生產者線程結束阻塞狀態。(後文講解ArrayBlockingQueue隊列的具體方法時,會涉及這些過程的詳細介紹)

4、主要方法

ArrayBlockingQueue隊列實現了java.util.concurrent.BlockingQueue接口,總的來說ArrayBlockingQueue隊列中的常用方法遵循相同的處理邏輯,區別點主要在於不能正常操作時的處理方式。這裏我們選擇幾個具有代表性的操作方法進行介紹:

4.1、offer(E e) 方法

根據官方的描述,offer(E e) 方法的主要工作過程是將特定的數據添加到隊列尾部,這個數據不能爲null。如果添加操作成功,則返回true,其他情況(添加失敗)則返回false。

public boolean offer(E e) {
  // 進行添加的數據對象,不能爲null
  Objects.requireNonNull(e);
  // 獲取隊列集合的操作權限
  final ReentrantLock lock = this.lock;
  lock.lock();
  try {
    // 如果條件成立,說明ArrayBlockingQueue隊列集合
    // 已經沒有多餘的空間進行添加操作,則返回false
    if (count == items.length) {
      return false;
    }
    
    // 這個else多餘了
    else {
      // 使用enqueue方法進行新的數據對象添加
      // 最後返回true
      enqueue(e);
      return true;
    }
  } finally {
    lock.unlock();
  }
}

4.2、put(E e) 方法

和offer(E e) 方法類似的,還有put(E e) 方法,兩者的區別是:如果ArrayBlockingQueue隊列集合不能(已經沒有多餘的空間)進行添加操作,那麼put(E e) 方法將進入阻塞狀態,直到被喚醒並能夠進行添加操作爲止。代碼片段如下:

/**
 * Inserts the specified element at the tail of this queue, waiting
 * for space to become available if the queue is full.
 */
public void put(E e) throws InterruptedException {
  Objects.requireNonNull(e);
  final ReentrantLock lock = this.lock;
  // 獲取到操作權後,才能進行後續操作
  lock.lockInterruptibly();
  try {
    // 如果條件成立,說明當前隊列中已經沒有空間進行添加操作
    // 那麼進入阻塞狀態
    while (count == items.length) {
      notFull.await();
    }
    // 通過enqueue方法進行添加操作
    enqueue(e);
  } finally {
    lock.unlock();
  }
}

lock.lock()方法和lock.lockInterruptibly()方法的區別,在之前介紹AQS的文章中已經進行了說明。這裏再做一次簡單說明:lockInterruptibly()方法在獲取鎖之前會確認線程中斷信號(Thread.interrupted()),如果收到線程中斷信號,則會拋出InterruptedException 異常;而lock()方法不會考慮線程中斷信號的問題。

4.3、E take() 方法

take()方法可以從ArrayBlockingQueue隊列頭部獲取一個數據對象,如果當前ArrayBlockingQueue隊列已經沒有數據對象可以獲取,則進入阻塞狀態。該方法中實際獲取數據對象的方法,是前文中已經介紹過的dequeue()方法。

public E take() throws InterruptedException {
  final ReentrantLock lock = this.lock;
  lock.lockInterruptibly();
  try {
    while (count == 0) {
      notEmpty.await();
    }
    return dequeue();
  } finally {
    lock.unlock();
  }
}

========
(ArrayBlockingQueue完,接後文《源碼閱讀(34):Java中線程安全的Queue、Deque結構——ArrayBlockingQueue(4)》)

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