一、分類
java中所有隊列都繼承至java.util.Queue接口,該接口定義了以下三組方法:
方法名 | 拋出異常 | 返回特殊值 |
---|---|---|
插入 | add(e) | offer(e) |
移除 | remove() | poll() |
檢查 | element() | peek() |
Java提供的線程安全的Queue可以分爲阻塞隊列和非阻塞隊列,使用阻塞算法的隊列可以用一個鎖(入隊和出隊用同一把鎖)或兩個鎖(入隊和出隊用不同的鎖)等方式來實現,而非阻塞的實現方式則可以使用循環CAS的方式來實現, 其中阻塞隊列的典型例子是BlockingQueue,非阻塞隊列的典型例子是ConcurrentLinkedQueue。
二、BlockingQueue 阻塞隊列
BlockingQueue 對插入操作、移除操作、獲取元素操作提供了四種不同的方法用於不同的場景中使用:
- 1、拋出異常;
- 2、返回特殊值(null 或 true/false,取決於具體的操作);
- 3、阻塞等待此操作,直到這個操作成功;
- 4、阻塞等待此操作,直到成功或者超時指定時間。總結如下:
– | 拋出異常 | 特殊值 | 阻塞 | 超時 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
移除 | remove() | poll() | take() | poll(time, unit) |
檢查 | element() | peek() | 不可用 | 不可用 |
從上表可以很明顯看出每個方法的作用,這個不用多說。我想說的是:
add(e) remove() element()
方法不會阻塞線程。當不滿足約束條件時,會拋出IllegalStateException 異常。例如:當隊列被元素填滿後,再調用add(e),則會拋出異常。offer(e) poll() peek()
方法即不會阻塞線程,也不會拋出異常。例如:當隊列被元素填滿後,再調用offer(e),則不會插入元素,函數返回false。- 要想要實現阻塞功能,需要調用
put(e) take()
方法。 當不滿足約束條件時,會阻塞線程。其實質可以用一個鎖(入隊和出隊共享一把鎖)來實現線程安全。以ArrayBlockQueue源碼中put(e)/take()源碼如下:
/**
* Inserts the specified element at the tail of this queue, waiting
* for space to become available if the queue is full.
*
* @throws InterruptedException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
/**
* 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;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
/**
* 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];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
BlockingQueue是個接口,有如下常用實現類:
- ArrayBlockQueue:一個由數組支持的有界阻塞隊列。此隊列按 FIFO(先進先出)原則對元素進行排序。創建其對象必須明確大小,像數組一樣。
- LinkedBlockQueue:一個可改變大小的阻塞隊列。此隊列按 FIFO(先進先出)原則對元素進行排序。創建其對象如果沒有明確大小,默認值是Integer.MAX_VALUE。鏈接隊列的吞吐量通常要高於基於數組的隊列,但是在大多數併發應用程序中,其可預知的性能要低。
- PriorityBlockingQueue:類似於LinkedBlockingQueue,但其所含對象的排序不是FIFO,而是依據對象的自然排序順序或者是構造函數所帶的Comparator決定的順序。
- SynchronousQueue:同步隊列。同步隊列沒有任何容量,每個插入必須等待另一個線程移除,反之亦然。
使用示例:
ArrayBlockQueue使用(生產者-消費者):https://www.jianshu.com/p/b1408e3e3bb4
三、ConcurrentLinkedQueue 非阻塞隊列
ConcurrentLinkedQueue是一個基於鏈接節點的無界線程安全隊列,它採用先進先出的規則對節點進行排序,當我們添加一個元素的時候,它會添加到隊列的尾部,當我們獲取一個元素時,它會返回隊列頭部的元素。基於CAS的“wait-free”(常規無等待)來實現,CAS並不是一個算法,它是一個CPU直接支持的硬件指令,這也就在一定程度上決定了它的平臺相關性。
再通過源碼來詳細分析下它是如何使用循環CAS的方式來入隊的(JDK1.8)
public boolean offer(E e) {
checkNotNull(e);
//創建入隊節點
final Node<E> newNode = new Node<E>(e);
//t爲tail節點,p爲尾節點,默認相等,採用失敗即重試的方式,直到入隊成功
for (Node<E> t = tail, p = t;;) {
//獲得p的下一個節點
Node<E> q = p.next;
// 如果下一個節點是null,也就是p節點就是尾節點
if (q == null) {
//將入隊節點newNode設置爲當前隊列尾節點p的next節點
if (p.casNext(null, newNode)) {
//判斷tail節點是不是尾節點,也可以理解爲如果插入結點後tail節點和p節點距離達到兩個結點
if (p != t)
//如果tail不是尾節點則將入隊節點設置爲tail。
// 如果失敗了,那麼說明有其他線程已經把tail移動過
casTail(t, newNode);
return true;
}
}
// 如果p節點等於p的next節點,則說明p節點和q節點都爲空,表示隊列剛初始化,所以返回 head節點
else if (p == q)
p = (t != (t = tail)) ? t : head;
else
//p有next節點,表示p的next節點是尾節點,則需要重新更新p後將它指向next節點
p = (p != t && t != (t = tail)) ? t : q;
}
}
public E poll() {
// 設置起始點
restartFromHead:
for (;;) {
//p表示head結點,需要出隊的節點
for (Node<E> h = head, p = h, q;;) {
//獲取p節點的元素
E item = p.item;
//如果p節點的元素不爲空,使用CAS設置p節點引用的元素爲null
if (item != null && p.casItem(item, null)) {
if (p != h) // hop two nodes at a time
//如果p節點不是head節點則更新head節點,也可以理解爲刪除該結點後檢查head是否與頭結點相差兩個結點,如果是則更新head節點
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
//如果p節點的下一個節點爲null,則說明這個隊列爲空,更新head結點
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
//結點出隊失敗,重新跳到restartFromHead來進行出隊
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
ConcurrentLinkedQueue 的非阻塞算法實現主要可概括爲下面幾點:
- 使用 CAS 原子指令來處理對數據的併發訪問,這是非阻塞算法得以實現的基礎。
- head/tail 並非總是指向隊列的頭 / 尾節點,也就是說允許隊列處於不一致狀態。 這個特性把入隊/出隊時,原本需要一起原子化執行的兩個步驟分離開來,從而縮小了入隊 /出隊時需要原子化更新值的範圍到唯一變量。這是非阻塞算法得以實現的關鍵。
- 以批處理方式來更新head/tail,從整體上減少入隊 / 出隊操作的開銷。
使用示例:
Java ConcurrentLinkedQueue隊列線程安全操作:https://yq.aliyun.com/articles/615890/
參考:
解讀 Java 併發隊列 BlockingQueue:https://javadoop.com/post/java-concurrent-queue
Java多線程高併發學習筆記——阻塞隊列:https://cloud.tencent.com/developer/article/1090012
Java線程安全隊列:https://www.jianshu.com/p/ad6ef76e067a
第二十一章、java線程安全隊列:https://www.jianshu.com/p/04aeb0088dec
java併發之ConcurrentLinkedQueue:https://www.jianshu.com/p/24516e7853d1
ConcurrentLinkedQueue的實現原理和源碼分析:https://www.jianshu.com/p/26d9745614dd
Java線程(十):CAS:https://www.kancloud.cn/digest/java-thread/107465