文章目錄
阻塞隊列、原子類原理分析
一、常用阻塞隊列
### 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);
}