阻塞隊列、原子類原理分析 -- ArrayBlockingQueue、AtomicInteger

阻塞隊列、原子類原理分析

一、常用阻塞隊列

	### 1.1 使用場景

​ 阻塞隊列比較普遍的使用場景是生產者、消費者, 以便於服務解耦,提高應用性能; 而分佈式架構應用比較頻繁的是消息隊列比如:Kafka、Rocketmq, 阻塞隊列類似於broker, 生產者和消費者類似於服務生成邏輯和消費邏輯,基於阻塞隊列解耦。同時阻塞隊列是一個FIFO隊列, 對於需要實現目標服務順序訪問的場景, 也可以使用。

  • 實現邏輯的異步解耦
  • 實現目標服務的順序訪問

1.2 常用的阻塞隊列

​ 下面是JDK提供的比較常用的阻塞隊列

阻塞隊列 說明
ArrayBlockingQueue 基於數組實現的有界阻塞隊列, 按照先進先出(FIFO)原則對元素進行排序。
DelayQueue 基於優先級隊列實現的阻塞隊列
LinkedBlockingDeque 基於鏈表實現的雙向阻塞隊列
LinkedBlockingQueue 基於鏈表實現的有界阻塞隊列, 默認最大長度是Integer.MAX_VALUE, 按照先進先出(FIFO)原則排序
LinkedTransferQueue 基於鏈表實現的無界阻塞隊列
PriorityBlockingQueue 基於優先級排序的無界阻塞隊列, 默認採用升序排序, 自定義排序規則實現方式, 1) 覆蓋compareTo方法實現 2) 初始化PriorityBlockingQueue時, 指定構造參數Comparator對元素進行排序
SynchronousQueue 不存儲元素的阻塞隊列, 每個put操作必須對應一個take操作, 否則不能繼續添加元素

在這裏插入圖片描述

1.3 阻塞隊列的常用方法

  • 插入操作
    • add(e), 添加元素到隊列中, 如果隊列滿了, 繼續插入元素會報IllegalStateException異常
    • offer(e), 添加元素到隊列中,返回元素是否插入成功, true – 成功, false – 失敗
    • offer(e, unit), 如果阻塞隊列滿了, 再繼續添加元素會被阻塞指定時間, 超時後線程直接退出
    • put(e), 如果阻塞隊列滿了, 再繼續添加元素, 線程會被阻塞, 直到隊列可用
  • 移除操作
    • remove(), 當隊列爲空時, 刪除會返回false;如果刪除元素成功, 返回true
    • poll(), 如果隊列存在元素, 則從隊列取出一個元素, 如果隊列爲空, 返回null
    • poll(time, unit), 如果隊列爲空, 會等待指定超時時間再去獲取元素
    • take(),如果隊列爲空,再獲取元素線程會被阻塞, 直到隊列可用

二、ArrayBlockingQueue原理分析

​ ArrayBlockingQueue是基於數組、ReentrantLock、Condition實現的阻塞隊列, 它的**數據結構是 數組 + 等待隊列**構成, 下面看下構造方法

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(); // 初始化非滿阻塞隊列
}
  • capacity: 表示數組長度, 也就是隊列長度
  • fair: 表示是否爲公平的阻塞隊列, 默認false, 表示基於非公平鎖實現的阻塞隊列

2.1 添加操作

add(e)

//1. 添加元素, 返回boolean結果
public boolean add(E e) {
  return super.add(e); // 調用父類add方法
}

//2. AbstractQueue添加元素的方法
public boolean add(E e) {
  if (offer(e)) // 添加成功返回true
    return true;
  else // 添加失敗, 拋出異常
    throw new IllegalStateException("Queue full");
}

​ 這裏使用了模版設計模式, 以ArrayBlockingQueue的add方法作爲入口, 實際調用的是父類的add方法, 這樣做的目的是解決通用型問題。

​ add內部調用了offer方法, 用於判斷隊列是否滿了, 如果offer返回true表示添加成功, 如果返回false,表示隊列滿了,會拋出IllegalStateException異常

offer(e)

public boolean offer(E e) {
  checkNotNull(e); // 檢查元素e是否爲空
  final ReentrantLock lock = this.lock; // 添加重入鎖
  lock.lock();
  try {
    if (count == items.length) // 列表滿了, 返回false, 不用等待, 注意和put區別
      return false;
    else { // 隊列沒滿, 進行入隊操作
      enqueue(e);
      return true;
    }
  } finally {
    lock.unlock(); // 釋放鎖
  }
}

​ offer方法根據是否添加成功返回對應的邏輯值, true – 成功, false – 失敗

  • 校驗添加的元素是否爲空
  • 添加重入鎖, 進行加鎖、解鎖操作
  • 校驗阻塞隊列是否已經滿了,
    • 如果滿了返回false
    • 如果沒滿進行入隊操作(enqueue)
  • 釋放獲取的鎖

enqueue(e)

/**
	* Inserts element at current put position, advances, and signals.
  * Call only when holding lock.
  */
private void enqueue(E x) {
  // assert lock.getHoldCount() == 1;
  // assert items[putIndex] == null;
  final Object[] items = this.items; 
  items[putIndex] = x; // 通過putIndex索引對數據賦值
  if (++putIndex == items.length) // 當putIndex等於數組長度時, 將putIndex重置爲0, 避免數組下標越界
    putIndex = 0;
  count++; // 記錄隊列中元素個數
  notEmpty.signal(); // 元素入隊列成功, 發出signal通知
}

​ add、offer方法添加元素到阻塞隊列中的核心處理方法, 線程持有鎖後, 通過putIndex索引直接將元素添加到數組items, 這裏有兩個核心地方

  • putIndex == item.length時, 需要將putIndex重置爲0, 避免下標越界
  • 添加元素後需要通過 notEmpty.signal發送通知, 讓等待的消費線程可以繼續執行, 因爲獲取(take)元素時, 如果隊列爲空會執行notEmpty.await進行等待

put(e)

public void put(E e) throws InterruptedException {
  checkNotNull(e); // 檢查元素是否爲空
  final ReentrantLock lock = this.lock; // 獲得鎖
  /**
  	* 注意和lock.lock()方法的區別
  	* lockInterruptibly方法允許在等待時, 由其它線程調用interrupt進行中斷
  	* lock方法是嘗試獲得鎖成功後才響應中斷
  	*/
  lock.lockInterruptibly();
  try {
    while (count == items.length) // 檢查隊列是否已經滿了, 如果滿了notFull進行等待
      notFull.await();
    enqueue(e); // 隊列未滿, 進行入隊操作
  } finally {
    lock.unlock(); // 釋放鎖
  }
}

​ put方法和offer、add方法一樣都是添加元素的方法, 注意它們之間的區別

  • add方法, 內部調用的是offer, 如果隊列滿了, 拋出IllegalStateException異常, 即offer返回false
  • offer方法, if檢測, 如果隊列滿了, 會直接返回false; 如果是offer(e, time, unit)隊列滿時需等待
  • put方法, while循環檢測, 如果隊列滿了, notFull.await()等待, 當隊列未滿時繼續執行入隊操作

2.2 刪除操作

remove

//AbstractQueue.remove
public E remove() {
  E x = poll(); // 內部調用poll獲取隊列第一個有效元素
  if (x != null) // 元素x不爲空, 直接返回這個元素
    return x;
  else // x==null,拋出異常
    throw new NoSuchElementException();
}

​ 內部調用poll方法, 如果獲取到元素直接返回這個元素, 如果沒有獲取到元素拋出NoSuchElementException異常

poll

//1. 獲取元素
public E poll() {
  final ReentrantLock lock = this.lock; // 獲取鎖對象
  lock.lock();
  try {
    return (count == 0) ? null : dequeue(); // 如果隊列不爲空, dequeue返回元素, 否則返回null
  } finally {
    lock.unlock(); // 釋放佔有的鎖
  }
}

//2. 帶超時時間的獲取元素
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
  long nanos = unit.toNanos(timeout);
  final ReentrantLock lock = this.lock; // 獲取重入鎖
  /**
  	* 注意和lock.lock()方法的區別
  	* lockInterruptibly方法允許在等待時, 由其它線程調用interrupt進行中斷
  	* lock方法是嘗試獲得鎖成功後才響應中斷
  	*/
  lock.lockInterruptibly();
  try {
    while (count == 0) {
      if (nanos <= 0)
        return null;
      nanos = notEmpty.awaitNanos(nanos); // 超時等待, notEmpty等待nanos時間
    }
    return dequeue(); // 入隊操作
  } finally {
    lock.unlock(); // 釋放持有的鎖
  }
}
  • 獲取重入鎖, 進行加鎖、解鎖操作
  • 校驗阻塞隊列是否爲空
    • 如果爲空, 返回null
    • 如果不爲空, 進行dequeue操作, 返回對應元素
  • 釋放獲取的鎖

NOTE: 分析上面代碼, 需要注意poll()和poll(time, unit)的區別, poll(time, unit)會進行超時等待, 而poll會直接返回。

take

public E take() throws InterruptedException {
  final ReentrantLock lock = this.lock; // 獲取鎖
  /**
  	* 注意和lock.lock()方法的區別
  	* lockInterruptibly方法允許在等待時, 由其它線程調用interrupt進行中斷
  	* lock方法是嘗試獲得鎖成功後才響應中斷
  	*/
  lock.lockInterruptibly();
  try {
    while (count == 0) // 隊列爲空, 進行等待操作
      notEmpty.await();
    return dequeue(); // 隊列非空, dequeue獲取元素
  } finally {
    lock.unlock(); // 釋放持有的鎖
  }
}

​ 阻塞式獲取隊列中元素的方法, 注意和poll()、poll(time, unit)的區別, 這個阻塞是可中斷的, 這點和poll(time, unit)一樣, 如果隊列沒有元素會notEmpty.await阻塞, 當隊列中有元素時會繼續執行; 這裏可以和入隊操作相對應, 在入隊時會調用enqueue, 如果入隊成功會調用notEmpty.signal進行通知,喚醒take阻塞的線程繼續執行。

dequeue

/**
 * Extracts element at current take position, advances, and signals.
 * 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]; // 默認獲取下標爲0的數據
    items[takeIndex] = null; // 將該位置的元素設置爲null
    if (++takeIndex == items.length) // 當takeIndex == 數組長度是, 重置takeIndex, 避免數組越界
        takeIndex = 0;
    count--; // 數組數量減1
    if (itrs != null)
        itrs.elementDequeued(); // 更新迭代器中的元數據
    notFull.signal(); // 發送數組未滿的信號, 讓因爲數組滿而阻塞的線程可以繼續執行添加操作
    return x;
}

​ dequeue是出隊列的核心方法, 主要是刪除隊列頭部元素,並返回給調用者, takeIndex記錄獲取數據的索引值,可以和putIndex進行比較。

​ itrs.elementDequeued, 更新迭代器中的元數據, 從前面阻塞方法類結構圖可以看出, ArrayBlockingQueue的父類AbstractCollection含有抽象方法iterator, ArrayBlockingQueue對這個方法進行了實現, 所以實現了迭代器的功能。

三、原子操作類

​ 操作的原子性, 是指一個操作或者一系列操作要麼同時成功, 要麼同時失敗, 不允許部分成功,部分失敗,比如我們常見的 i++, 是一個非原子操作。

3.1 原子類的分類

  • 原子更新基本類型
    • AtomicBoolean、AtomicInteger、AtomicLong
  • 原子更新數組
    • AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
  • 原子更新引用
    • AtomicReference、AtomicReferenceFieldUpdater、AtomicMarkableReference
  • 原子更新字段
    • AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference

3.2 原理分析

unsafe

​ unsafe在很多場景中都有使用, 比如:

  • 多線程同步(MonitorEnter)
  • CAS操作(compareAndSwap)
  • 線程的掛起和恢復(park、unpark)
  • 內存屏障(loadFence、storeFence)
  • 原子類操作(getAndIncrement、getAndAdd)

​ 這裏以AtomicInteger爲例說明原子操作的實現原理, 原子操作的實現是基於unsafe來實現基本操作的, 具體查看下面內容

getAndIncrement

public final int getAndIncrement() {
  return unsafe.getAndAddInt(this, valueOffset, 1);
}

getAndAdd

public final int getAndAdd(int delta) {
  return unsafe.getAndAddInt(this, valueOffset, delta);
}

compareAndSet

public final boolean compareAndSet(int expect, int update) {
  return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章