13. ConcurrentLinkedQueue (循環CAS)
應用場景:
按照適用的併發強度從低到高排列如下:
- LinkedList/ArrayList 非線程安全,不能用於併發場景(List的方法支持棧和隊列的操作,因此可以用List封裝成stack和queue);
- Collections.synchronizedList 使用wrapper class封裝,每個方法都用synchronized(mutex:Object)做了同步
- LinkedBlockingQueue 採用了鎖分離的設計,避免了讀/寫操作衝突,且自動負載均衡,可以有界。BlockingQueue在生產-消費模式下首選【Iterator安全,不保證數據一致性】
- ConcurrentLinkedQueue 適用於高併發讀寫操作,理論上有最高的吞吐量,無界,不保證數據訪問實時一致性,Iterator不拋出併發修改異常,採用CAS機制實現無鎖訪問。
綜上:
- 在併發的場景下,如果併發強度較小,性能要求不苛刻,且鎖可控的場景下,可使用Collections.synchronizedList,既保證了數據一致又保證了線程安全,性能夠用;
- 在大部分高併發場景下,建議使用 LinkedBlockingQueue ,性能與 ConcurrentLinkedQueue 接近,且能保證數據一致性;
- ConcurrentLinkedQueue 適用於超高併發的場景,但是需要針對數據不一致採取一些措施。
源碼分析
offer(E e)
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;
}
}
即定位出尾節點=>CAS入隊=>重新定位tail節點。
poll( )
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;
}
}
}
即獲取head節點的元素 => 判斷head節點元素是否爲空=>如果爲空,表示另外一個線程已經進行了一次出隊操作將該節點的元素取走=>如果不爲空,則使用CAS的方式將head節點的引用設置成null=>如果CAS成功,則直接返回head節點的元素=>如果CAS不成功,表示另外一個線程已經進行了一次出隊操作更新了head節點,導致元素髮生了變化,需要重新獲取head節點=>如果p節點的下一個節點爲null,則說明這個隊列爲空(此時隊列沒有元素,只有一個僞結點p),則更新head節點。
特點
- 訪問操作採用了無鎖設計
- Iterator的弱一致性,即不保證Iteartor訪問數據的實時一致性(與current組的成員與COW成員類似)
- 併發offer/poll
注意事項
size操作需要遍歷整個隊列,且如果此時queue正在被修改,size可能返回不準確的數值(仍然是無法保證數據一致性),這是一個非常耗時的操作,判斷隊列是否爲空建議使用isEmpty()。如果需要保證數據一致性,頻繁獲取集合對象的size,最好不使用concurrent族的成員。
批量操作(bulk operations like addAll,removeAll,equals)無法保證原子性,因爲不保證實時性,且沒有使用獨佔鎖的設計。例如,在執行addAll的同時,有另外一個線程通過Iterator在遍歷,則遍歷的線程可能只看到一部分新增的數據。
ConcurrentLinkedQueue 沒有實現BlockingQueue接口。當隊列爲空時,take方法返回null,此時consumer會需要處理這個情況,consumer會循環調用take來保證及時獲取數據,此爲busy waiting,會持續消耗CPU資源。
與 LinkedBlockingQueue 的對比
- LinkedBlockingQueue 採用了鎖分離的設計,put、get鎖分離,保證兩種操作的併發;
- 當隊列爲空/滿時,某種操作會被掛起;
- 兩者的Iterator都不不保證數據一致性,Iterator遍歷的是Iterator創建時已存在的節點,創建後的修改不保證能反應出來。
- LinkedBlockingQueue 的size是在內部用一個AtomicInteger保存,執行size操作直接獲取此原子量的當前值,時間複雜度O(1)。
ConcurrentLinkedQueue 的size操作需要遍歷(traverse the queue),因此比較耗時,時間複雜度至少爲O(n),建議使用isEmpty()。