ConcurrentLinkedQueue是無阻塞佇列的一種實現, 依賴與CAS演算法實現。
入隊offer
-
if(q==null)
當前是尾節點 -> CAS賦值tail.next = newNode, 成功就跳出迴圈 -
elseif(p == q)
尾節點被移除 -> 從tail或head重新往後找 -
else
不是尾節點 -> 往next找
規則定義:
當一個節點的next指向自身時, 表示節點已經被移除, 註釋中還會強調這一點。
完整程式碼(JDK8):
/**
* Inserts the specified element at the tail of this queue.
* As the queue is unbounded, this method will never return {@code false}.
*
* @return {@code true} (as specified by {@link Queue#offer})
* @throws NullPointerException if the specified element is null
*/
/*
* 變數說明:
* 成員變數:
* head: 首節點
* tail: 尾節點, 不一定指向末尾, 兩次入隊才更新一次
* 區域性變數
* t= tail; //儲存迴圈開始時, 當時的tail值
* p= t; // 每次查詢的起始位置, 可能指向首節點head或者臨時尾節點t
* q= p.next; // 每次迴圈下一個節點
* newNode= new Node; // 新節點
*
*
* 重要概念:
* 當p = p.next時, 表示節點已經被移除
*/
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) { // 情況1: p是尾節點
// p is last node
// p是尾節點就直接將新節點放入末尾
if (p.casNext(null, newNode)) {
// Successful CAS is the linearization point
// for e to become an element of this queue,
// and for newNode to become "live".
if (p != t) // hop two nodes at a time // 一次跳兩個節點, 即插入兩次, tail更新一次
casTail(t, newNode); // Failure is OK. // 失敗也無妨, 說明被別的執行緒更新了
return true; // 退出迴圈
}
// Lost CAS race to another thread; re-read next
}
else if (p == q) // 情況2: p節點被刪除了
// We have fallen off list. If tail is unchanged, it
// will also be off-list, in which case we need to
// jump to head, from which all live nodes are always
// reachable. Else the new tail is a better bet.
// 儲存的節點p、t都已經失效了,這時需要重新檢索,重新檢索的起始位置有兩種情況
// 1.1. 如果tail==t,表示tail也是失效的, 那麼從head開始找
// 1.2. 否則tail就是被其他執行緒更新了, 可以又試著從tail找
p = (t != (t = tail)) ? t : head;
else // 情況3: 沿著p往下找
// Check for tail updates after two hops.
// 這段簡單看作p = q就好理解了, 這麼寫是為了提高效率:
// 1. 情況二中p可能指向了head(由於tail節點失效導致的)
// 2. 現在tail可能被其他執行緒更新,也許重新指向了隊尾
// 3. 如果是, 嘗試則從隊尾開始找, 以減少迭代次數
p = (p != t && t != (t = tail)) ? t : q;
}
}
這兩段程式碼看了很久, 特別記錄下:
-
情況2中的
p = (t != (t = tail)) ? t : head;
(t != (t = tail))
可以分三步來看1.1. 首先取出t 1.2. 將tail賦值給t 1.3. 將先前取出的t與更新後的t比較
-
情況3中
p = (p != t && t != (t = tail)) ? t : q;
首先: p != t: 這種情況只有可能發生在執行了情況2後
現狀: 這時p指向head或者中間的元素, t指向一個被刪除了的節點
那麼如果tail被其他執行緒更新了, 我們可以將t重新指向tail, p指向t, 就像剛進迴圈一樣, 從尾節點開始檢索。
這樣比從head往後找更有效率
出隊poll
規則定義:
補充一項, item==null,也表示節點已經被刪除(參考remove方法)。
/**
* updateHead
*
*/
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
if (item != null && p.casItem(item, null)) {
// Successful CAS is the linearization point
// for item to be removed from this queue.
if (p != h) // hop two nodes at a time
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
/**
* Tries to CAS head to p. If successful, repoint old head to itself
* as sentinel for succ(), below.
*/
final void updateHead(Node<E> h, Node<E> p) {
if (h != p && casHead(h, p))
h.lazySetNext(h);
}
出隊設值操作:
先更新head, 再將舊head的next指向自己
Note:
CAS演算法實現依靠Unsafe.compareAndSwapObject實現
UNSAFE.compareAndSwapObject(物件, 欄位偏移量, 當前值, 新值)
可以為物件中的某個欄位實現CAS操作
lazySet依賴UNSAFE.putOrderedObject實現
UNSAFE.putOrderedObject(物件, 欄位偏移量, 新值)
這個只能用在volatile欄位上
個人理解: volatile的設值會導致本地快取失效, 那麼需要重新從主存讀取, 使用這個方法可以使暫存器快取依舊有效, 不必急於從主存取值。
使用目的: 移除節點時, 需要更新節點的next指向自身, 但現在next指向的資料實際是有效的; 高併發情況下,如果offser方法已經快取了這個next值, 直接設定next會導致快取行失效, CPU需要重新讀取next; 而使用putOrderedObject可以讓offser從這個next繼續檢索