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

1、概述

ArrayBlockingQueue是一種經常使用的線程安全的Queue結構,上文也已經提過,它是一種內部基於數組的,使用在高併發場景下的阻塞隊列,也是一種容量有界的隊列。該隊列符合先進先出(FIFO)的工作原則,也就是說該隊列頭部的元素是最先進入隊列集合的,也是最先被調用者取出的元素;該隊列尾部的元素是最後進入隊列集合的,也是按時間順序會最後被調用者取出的元素。

在多線程同時讀寫ArrayBlockingQueue隊列集合中的元素時,該隊列還支持一種公平性策略,這是一種爲生產者/消費者工作模式提供的配置模式(可以把ArrayBlockingQueue隊列集合的讀取操作線程看成消費者角色;把寫入操作線程看成生產者角色),一旦啓用了這個配置,則ArrayBlockingQueue隊列會分別保證多個生產者線程和多個消費者線程獲取ArrayBlockingQueue操作權限的順序——先請求操作的線程會先獲得操作權限(後文會給出示例)。
在這裏插入圖片描述
該隊列的公平性策略實際上基於ReentrantLock——基於AQS機制的可重入鎖的公平性功能,下面我們描述幾種使用ArrayBlockingQueue的基本場景。

1.1、ArrayBlockingQueue的最基礎使用

// ......
// 設置一個最大容量爲5的隊列
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
// 前文已經介紹過,add方法是在Queue接口定義的方法(BlockingQueue接口中有重複的定義)
// 其作用沒有阻塞線程的特性,如果add方法發現隊列集合的容量已經達到上限,就會拋出異常
queue.add("1");
queue.add("2");
queue.add("3");
queue.add("4");
try {
  // put方法是在BlockingQueue接口中定義的方式,它有阻塞線程的特點
  // 如果put方法發現隊列集合的容量達到上限,就會阻塞線程
  queue.put("5");
  // 也就是說該代碼的工作位置,線程將被阻塞
  queue.put("6");
} catch(InterruptedException e) {
  Thread.currentThread().interrupt();
  e.printStackTrace(System.out);
}
// ......  

以上代碼所示,是一種在單個線程場景下,演示的ArrayBlockingQueue隊列集合的使用方式。當然按照之前內容的介紹,我們並不推薦在單線程場景下使用實現了BlockingQueue接口的集合,正式的業務編碼活動中,完全可以使用ArrayList這樣的集合進行替代。

1.2、在多線程操作場景下使用ArrayBlockingQueue

包括ArrayBlockingQueue在內的所有實現了BlockingQueue接口的隊列集合,其多線程下的典型使用場景是生產者和消費者操作場景——由一個或多個生產者進行數據生產,然後按照隊列特性放入隊列中,並由另一個或多個消費者從隊列中取出後進行處理。而不同的阻塞式隊列對生產者如何放入元素、元素在隊列中如何排列、消費者如何取出元素的規則都有不同的約定特點。

例如ArrayBlockingQueue對於生產者如何放入隊列的規定就可以滿足,如果當前隊列中沒有多餘的空間可供生產者們向隊列添加元素,那麼生產者就可以被阻塞起來,直到隊列中有新的空間出現;另外該隊列還提供在出現上述場景時,生產者直接拋出異常的處理方式。

下面我們看一下基於ArrayBlockingQueue實現生產者和消費者的典型代碼:

  • 生產者線程代碼:
/**
 * 生產者,非常簡單,不用過多註釋說明
 * @author yinwenjie
 */
public static class Producer implements Runnable {
  // 生產者生產的數據,將放入該隊列
  private BlockingQueue<String> queue;
  
  public Producer(BlockingQueue<String> queue) {
    this.queue = queue;
  }

  @Override
  public void run() {
    String uuid = UUID.randomUUID().toString();
    int count = 0;
    while(count++ < Integer.MAX_VALUE) {
      // 如果不能添加到隊列,則本生產者線程阻塞等待
      try {
        this.queue.put(uuid);
      } catch (InterruptedException e) {
        e.printStackTrace(System.out);
      }
    }
  }
}
  • 消費者線程代碼
/**
 * 消費者,代碼也很簡單,也不用過多註釋說明
 * @author yinwenjie
 */
public static class Consumer implements Runnable {
  // 消費者將從該隊列中取出數據進行處理
  private BlockingQueue<String> queue;
  
  public Consumer(BlockingQueue<String> queue) {
    this.queue = queue;
  }
  
  @Override
  public void run() {
    int count = 0;
    while(count++ < Integer.MAX_VALUE) {
      try {
        String value = this.queue.take();
        // 這裏省略處理過程
      } catch (InterruptedException e) {
        e.printStackTrace(System.out);
      }
    }
  }
}

請注意,以上給出的生產者代碼和消費者代碼適用於多種實現了BlockingQueue接口的隊裏集合的講解,後續文章中我們不會再給出生產者和消費者的基礎代碼。接下來使用以下代碼將生產者和消費者啓動起來。

  • 將消費者和生產者啓動起來
// ......
// 本文建議使用線程池管理線程,而不是直接創建Thread對象
ThreadPoolExecutor serviceExecutor = new ThreadPoolExecutor(10, 10, 1000, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
// 這個隊列用來承載連接生成者和消費者的數據關係,隊列容量上限100
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
// 提交5個生產者和5個消費者
serviceExecutor.submit(new Producer(queue));
serviceExecutor.submit(new Producer(queue));
serviceExecutor.submit(new Producer(queue));
serviceExecutor.submit(new Producer(queue));
serviceExecutor.submit(new Producer(queue));
serviceExecutor.submit(new Consumer(queue));
serviceExecutor.submit(new Consumer(queue));
serviceExecutor.submit(new Consumer(queue));
serviceExecutor.submit(new Consumer(queue));
serviceExecutor.submit(new Consumer(queue));
// ......

線程池本身的使用,並不在本文描述的內容範圍內,如果需要進行詳細瞭解,可以查看此篇文章《線程基礎:線程池(5)——基本使用(上)》、《線程基礎:線程池(6)——基本使用(中)》、《線程基礎:線程池(7)——基本使用(下)

1.3、使用ArrayBlockingQueue的公平性策略

在上一小節的代碼中,當隊列集合沒有多餘的存儲空間時,所有生產者線程都將先後進入阻塞狀態,並在隊列集合有空餘位置時被喚醒,但是ArrayBlockingQueue隊列集合並不保證線程喚醒的公平性,也就是說隊列集合並不保證最先進入阻塞狀態的生產者線程最先被喚醒

如果在一些特定的業務場景下,需要保證生產者線程和消費者線程的公平性原則,則需要啓用ArrayBlockingQueue的公平性策略,如下方式所示:

// ......
// if true then queue accesses for threads blockedon insertion or removal, are processed in FIFO order;
// if false the access order is unspecified.
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100, true);
// ......

2、工作原理

實際上ArrayBlockingQueue是一個可循環使用數組空間的有界、阻塞隊列,使用可複用的環形數組進行數據記錄。其內部使用一個takeIndex變量代表隊列頭(隊列頭可在數組的任何有效索引位),使用一個putIndex變量代碼隊列尾(隊列爲可不是數組最後一個索引位);從takeIndex到putIndex的索引位置,是數組中已經放置了元素的位置,從putIndex到takeIndex的索引位置是數組中還可以放置新的元素的位置。

這句話是不是不好理解,請看以下原理圖:
在這裏插入圖片描述
從上圖可以看出ArrayBlockingQueue的數組構成了一個環形結構,使得數組本身“首尾相連”。takeIndex變量指向的索引位是下一個將被取出的元素索引位,putIndex變量指向的索引位是下一個將被添加的元素索引位。爲了支撐這個環形結構的工作,ArrayBlockingQueue隊列集合還使用了很多輔助的變量信息。如下小節所述:

2.1、ArrayBlockingQueue的主要變量

public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
  // 這個數據就是ArrayBlockingQueue隊列集合用來存儲數據的數組
  final Object[] items;
  // 該變量指向的索引位上的元素是下一次將從隊列集合中移除的元素,這個移除操作可以是take, poll, peek 或者 remove。
  int takeIndex;
  // 該變量指向的索引位是下一個添加到隊列的元素存儲的索引位,這個添加操作可以是 put, offer 或者 add
  int putIndex;
  // 該變量標示當前在隊列集合中的總的元素數量
  int count;
  // ArrayBlockingQueue隊列使用基於AQS機制的可重入鎖ReentrantLock進行線程安全性控制
  // 並採用雙條件控制方式對移除、添加操作進行交互控制
  final ReentrantLock lock;
  // 控制着移除操作條件
  private final Condition notEmpty;
  // 控制着添加操作條件
  private final Condition notFull;
  // ArrayBlockingQueue的迭代器(以及後續幾個隊列集合的迭代器)很有趣,我們將在後文專門剖析
  transient Itrs itrs = null;
}

這裏特別注意兩個Condition對象,notEmpty條件對象負責在隊列集合變爲非空的場景下,進行生產者線程和消費者線程的工作協調,具體來說就是給消費者線程發信號,告訴它們線程隊列集合中又有新的數據可以取出了;notFull條件對象負責在隊列集合變爲非滿的場景下,進行生產者線程和消費者線程的工作協調,具體來說就是給生產者線程發信號,告訴它們線程隊列集合中又有新的索引位可以放置新的數據了。

2.2、環形隊列的入隊和出隊過程

實際上在ArrayBlockingQueue隊列集合中負責向隊列添加數據的方法只有一個,就是enqueue()方法;向隊列移除數據的方法也只有一個,就是dequeue()方法。基本上ArrayBlockingQueue隊列向外暴露的操作方法,都是對上述兩個方法的封裝調用。所以我們首先需要剖析這兩個方法:

  • enqueue()方法
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
  implements BlockingQueue<E>, java.io.Serializable {

  // ......
  /**
   * 該方法負責在putIndex變量指定的索引位置添加新的數據
   * 該方法內部雖然沒有做線程安全性的操作,但是對該方法的調用者都有“持有鎖”的要求:
   * Call only when holding lock.
   */
  private void enqueue(E x) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    // 將入參的x元素添加到指定的數組索引位置
    items[putIndex] = x;
    // 添加後,如果下一個索引位超出了邊界,則索引位置重新指向0
    if (++putIndex == items.length)
      putIndex = 0;
    // 集合總數據量的計數器 + 1
    count++;
    // 發出信號,幫助那些在集合爲空時處於阻塞狀態的線程(消費者線程),解除阻塞狀態
    notEmpty.signal();
  }
  // ......  
}

這個方法很簡單,將新的元素添加到隊列集合的操作過程歸納爲一句話就是,在putIndex的索引位置放入新的元素,並將putIndex指向的索引位向後移動一位,如果移動後超出數組邊界,則重新指向0號索引位。

  • dequeue()方法
// TODO 繼續增加註釋
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
  implements BlockingQueue<E>, java.io.Serializable {

  // .....
  /**
   * 該方法負責從takeIndex指向的索引位移除一個元素
   * 該方法內部雖然沒有做線程安全性的操作,但是對該方法的調用者都有“持有鎖”的要求:
   * Call only when holding lock.
   */
  private E dequeue() {
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    // 將已經移除了數據的索引位置爲null,以便幫助可能的GC動作
    items[takeIndex] = null;
    // 移除後,如果下一個索引位超出了邊界,則索引位置重新指向0
    if (++takeIndex == items.length)
      takeIndex = 0;
    // 集合總數據量的計數器 - 1
    count--;
    // 如果存在迭代器(們),則迭代器也需要進行數據清理
    if (itrs != null)
      itrs.elementDequeued();
    // 發出信號,幫助那些在集合已滿時進入阻塞狀態的線程(生產者線程),解除阻塞狀態
    notFull.signal();
    return x;
  }
  // .....
}

這個方法也很簡單,將隊列集合移除數據的操作過程歸納爲一句話就是:在takeIndex的索引位的數據將被移除,並將takeIndex指向的索引位向後移動一位,如果移動後超出數組邊界,則重新指向0號索引位。另外,再次強調,enqueue方法和dequeue方法都沒有做線程安全性控制,而需要這兩個方法的調用者自行控制線程安全。

============================
(接下文《源碼閱讀(32):Java中線程安全的Queue、Deque結構——ArrayBlockingQueue(2)》)

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