ConcurrentLinkedDeque 是雙向鏈表結構的無界并發隊列。從JDK 7開始加入到J.U.C的行列中。使用CAS實現并發安全,與 ConcurrentLinkedQueue 的區別是該阻塞隊列同時支持FIFO和FILO兩種操作方式,即可以從隊列的頭和尾同時操作(插入/刪除)。適合“多生產,多消費”的場景。內存一致性遵循對 ConcurrentLinkedDeque 的插入操作先行發生于(happen-before)訪問或移除操作。相較于 ConcurrentLinkedQueue,ConcurrentLinkedDeque 由于是雙端隊列,所以在操作和概念上會更加復雜。
概述
ConcurrentLinkedDeque(后面稱CLD) 的實現方式繼承了 ConcurrentLinkedQueue 和 LinkedTransferQueue的思想,在非阻塞算法的實現方面與 ConcurrentLinkedQueue 基本一致。關于 ConcurrentLinkedQueue,請參考筆者的上一篇文章:JUC源碼分析-集合篇(三):ConcurrentLinkedQueue。
數據結構
重要屬性:
//頭節點
private transient volatile Node<E> head;
//尾節點
private transient volatile Node<E> tail;
//終止節點
private static final Node<Object> PREV_TERMINATOR, NEXT_TERMINATOR;
//移除節點時更新鏈表屬性的閥值
private static final int HOPS = 2;
和ConcurrentLinkedQueue一樣,CLD 內部也只維護了head
和tail
屬性,對 head/tail 節點也使用了“不變性”和“可變性”約束,不過跟 ConcurrentLinkedQueue 有些許差異,我們來看一下:
head/tail 的不變性:
- 第一個節點總是能以O(1)的時間復雜度從 head 通過 prev 鏈接到達;
- 最后一個節點總是能以O(1)的時間復雜度從 tail 通過 next 鏈接到達;
- 所有live節點(item不為null的節點),都能從第一個節點通過調用 succ() 方法遍歷可達;
- 所有live節點(item不為null的節點),都能從最后一個節點通過調用 pred() 方法遍歷可達;
- head/tail 不能為 null;
- head 節點的 next 域不能引用到自身;
- head/tail 不會是GC-unlinked節點(但它可能是unlink節點)。
head/tail的可變性:
- head/tail 節點的 item 域可能為 null,也可能不為 null;
- head/tail 節點可能從first/last/tail/head 節點訪問時不可達;
- tail 節點的 next 域可以引用到自身。
注:CLD中也對 head/tail 的更新也使用了“松弛閥值”的概念(在 ConcurrentLinkedQueue 一篇中已經分析),除此之外,CLD設定了一個“跳躍閥值”-HOPS(指在查找活動節點時跳過的已刪除節點數),在執行出隊操作時,跳躍節點數大于2或者操作的節點不是 first/last 節點時才會更新鏈表(后面源碼中詳細分析)。
除此之外,再來看看CLD中另外兩個屬性:
-
PREV_TERMINATOR:prev的終止節點,next指向自身,即
PREV_TERMINATOR.next = PREV_TERMINATOR
。在 first 節點出列后,會把first.next指向自身(first.next=first
),然后把prev設為PREV_TERMINATOR
。 -
NEXT_TERMINATOR:next的終止節點,prev指向自身,即
NEXT_TERMINATOR.pre = NEXT_TERMINATOR
。在 last 節點出列后,會把last.prev指向自身(last.prev=last
),然后把next設為
NEXT_TERMINATOR
。
源碼解析
在開始源碼分析之前,我們先來看一下CLD中對Node的定義:
- live node:節點的 item!=null 被稱為live節點。當節點的 item 被 CAS 改為 null,邏輯上來講這個節點已經從鏈表中移除;一個新的元素通過 CAS 添加到一個包含空 prev 或空 next 的 first 或 last 節點,這個元素的節點在這時是 live節點。
- first node & last node:首節點(first node)總會有一個空的 prev 引用,終止任何從 live 節點開始的 prev 引用鏈;同樣的最后一個節點(last node)是 next 的終止節點。first/last 節點的 item 可以為 null。并且 first 和 last 節點總是相互可達的。
-
active node:live節點、first/last 節點也被稱為活躍節點(active node),活躍節點一定是被鏈接的,如果p節點為active節點,則:
p.item != null || (p.prev == null && p.next != p) || (p.next == null && p.prev != p)
- self-node:自鏈接節點,prev 或 next 指向自身的節點,自鏈接節點用在解除鏈接操作中,并且它們都不是active node。
- head/tail節點:head/tail 也可能不是 first/last 節點。從 head 節點通過 prev 引用總是可以找到 first 節點,從 tail 節點通過 next 引用總是可以找到 last 節點。允許 head 和 tail 引用已刪除的節點,這些節點沒有鏈接,因此可能無法從 live 節點訪問到。
節點刪除時經歷三個階段:邏輯刪除(logical deletion),未鏈接( unlinking),和gc未鏈接( gc-unlinking):
logical deletion:通過 CAS 修改節點 item 為 null 來完成,表示當前節點可以被解除鏈接(unlinking)。
unlinking: 這種狀態下的節點與其他 active 節點有鏈接,但是其他 active 節點與之都沒有鏈接,也就是說從這個狀態下的節點可以達到 active 節點,但是從 active 節點不可達到這種狀態的節點。在任何時候,從 first 通過 next 找到的 live 節點和從 last 通過 prev 找到的節點總是相等的。但是,在節點被邏輯刪除時上述結論不成立,這些被邏輯刪除的節點也可能只從一端是可達的。
gc-unlinking: GC未鏈接使已刪除節點不可達到 active 節點,使GC更容易回收被刪除的節點。通過讓節點自鏈接或鏈接到終止節點(PREV_TERMINATOR 或 NEXT_TERMINATOR)來實現。 gc-unlinking 節點從 head/tail 訪問不可達。這一步是為了使數據結構保持GC健壯性(gc-robust),防止保守式GC(conservative GC,目前已經很少使用)對這些邊界空間的使用。對保守式GC來說,使數據結構保持GC健壯性會消除內存無限滯留的問題,同時也提高了分代收機器的性能。
如果同學們對上述理論還是一頭霧水,那么從源碼解析中我們就可以直觀的看到它們的作用。
OK!準備工作已經做完,下面我們正式開始源碼解析。
添加(入列)
CLD的添加方法包括:offer(E)、add(E)、push(E)、addFirst(E)、addLast(E)、offerFirst(E)、offerLast(E)
,所有這些操作都是通過linkFirst(E)
或linkLast(E)
來實現的。
linkFirst(E) / linkLast(E)
/**
* Links e as first element.
*/
private void linkFirst(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
restartFromHead:
for (;;)
//從head節點往前尋找first節點
for (Node<E> h = head, p = h, q;;) {
if ((q = p.prev) != null &&
(q = (p = q).prev) != null)
// Check for head updates every other hop.
// If p == q, we are sure to follow head instead.
//如果head被修改,返回head重新查找
p = (h != (h = head)) ? h : q;
else if (p.next == p) // 自鏈接節點,重新查找
continue restartFromHead;
else {
// p is first node
newNode.lazySetNext(p); // CAS piggyback
if (p.casPrev(null, newNode)) {
// Successful CAS is the linearization point
// for e to become an element of this deque,
// and for newNode to become "live".
if (p != h) // hop two nodes at a time 跳兩個節點時才修改head
casHead(h, newNode); // Failure is OK.
return;
}
// Lost CAS race to another thread; re-read prev
}
}
}
/**
* Links e as last element.
*/
private void linkLast(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
restartFromTail:
for (;;)
//從tail節點往后尋找last節點
for (Node<E> t = tail, p = t, q;;) {
if ((q = p.next) != null &&
(q = (p = q).next) != null)
// Check for tail updates every other hop.
// If p == q, we are sure to follow tail instead.
//如果tail被修改,返回tail重新查找
p = (t != (t = tail)) ? t : q;
else if (p.prev == p) // 自鏈接節點,重新查找
continue restartFromTail;
else {
// p is last node
newNode.lazySetPrev(p); // CAS piggyback
if (p.casNext(null, newNode)) {
// Successful CAS is the linearization point
// for e to become an element of this deque,
// and for newNode to become "live".
if (p != t) // hop two nodes at a time 跳兩個節點時才修改tail
casTail(t, newNode); // Failure is OK.
return;
}
// Lost CAS race to another thread; re-read next
}
}
}
說明:linkFirst
是插入新節點到隊列頭的主函數,執行流程如下:
首先從 head 節點開始向前循環找到 first 節點(p.prev==null&&p.next!=p
);然后通過lazySetNext
設置新節點的 next 節點為 first;然后 CAS 修改 first 的 prev 為新節點。注意這里 CAS 指令成功后會判斷 first 節點是否已經跳了兩個節點,只有在跳了兩個節點才會 CAS 更新 head,這也是為了節省 CAS 指令執行開銷。linkLast
是插入新節點到隊列尾,執行流程與linkFirst
一致,不多贅述,具體見源碼。
注:lazySetNext
通過 Unsafe 類的putOrderedObject
實現,有關這個方法,請參考筆者的另一篇文章:JUC源碼分析—CAS和Unsafe。
獲取(出列)
CLD的獲取方法分兩種:
獲取節點:peek、peekFirst 、peekLast、getFirst、getLast
,都是通過peekFirst 、peekLast
實現。
獲取并移除節點: poll、pop、remove、pollFirst、pollLast、removeFirst、removeLast
,都是通過pollFirst、pollLast
實現。
pollFirst、pollLast
包括了peekFirst 、peekLast
的實現,都是找到并返回 first/last 節點,不同的是,pollFirst、pollLast
比peekFirst 、peekLast
多了 unlink 這一步。所以這里我們只對pollFirst
和pollLast
兩個方法進行解析。
首先來看一下pollFirst() :
pollFirst()
/**獲取并移除隊列首節點*/
public E pollFirst() {
for (Node<E> p = first(); p != null; p = succ(p)) {
E item = p.item;
if (item != null && p.casItem(item, null)) {
unlink(p);
return item;
}
}
return null;
}
說明: pollFirst()
用于找到鏈表中首個 item 不為 null 的節點(注意并不是first節點,因為first節點的item可以為null),并返回節點的item。涉及的內部方法較多,不過都很簡單,我們通過穿插代碼方式分析:
- 首先通過
first()
方法找到 first 節點,first 節點必須為 active 節點(p.prev==null&&p.next!=p
)。first()
源碼如下:
Node<E> first() {
restartFromHead:
for (;;)
//從head開始往前找
for (Node<E> h = head, p = h, q;;) {
if ((q = p.prev) != null &&
(q = (p = q).prev) != null)
// Check for head updates every other hop.
// If p == q, we are sure to follow head instead.
//如果head被修改則返回新的head重新查找,否則繼續向前(prev)查找
p = (h != (h = head)) ? h : q;
else if (p == h
// It is possible that p is PREV_TERMINATOR,
// but if so, the CAS is guaranteed to fail.
//找到的節點不是head節點,CAS修改head
|| casHead(h, p))
return p;
else
continue restartFromHead;
}
}
- 如果
first.item==null
(這里是允許的,具體見上面我們對 first/last 節點的介紹),則繼續調用succ
方法尋找后繼節點。succ
源碼如下:
/**返回指定節點的的后繼節點,如果指定節點的next指向自己,返回first節點*/
final Node<E> succ(Node<E> p) {
// TODO: should we skip deleted nodes here?
Node<E> q = p.next;
return (p == q) ? first() : q;
}
- CAS 修改節點的 item 為 null(即 “邏輯刪除-logical deletion”),然后調用
unlink(p)
方法解除節點鏈接,最后返回 item。unlink(p)
是移除節點的主方法,邏輯較為復雜,后面我們單獨分析。
unlink(Node<E> x)
/**
* Unlinks non-null node x.
*/
void unlink(Node<E> x) {
// assert x != null;
// assert x.item == null;
// assert x != PREV_TERMINATOR;
// assert x != NEXT_TERMINATOR;
final Node<E> prev = x.prev;
final Node<E> next = x.next;
if (prev == null) {//操作節點為first節點
unlinkFirst(x, next);
} else if (next == null) {//操作節點為last節點
unlinkLast(x, prev);
} else {// common case
Node<E> activePred, activeSucc;
boolean isFirst, isLast;
int hops = 1;
// Find active predecessor
//從被操作節點的prev節點開始找到前繼活動節點
for (Node<E> p = prev; ; ++hops) {
if (p.item != null) {
activePred = p;
isFirst = false;
break;
}
Node<E> q = p.prev;
if (q == null) {
if (p.next == p)
return;//自鏈接節點
activePred = p;
isFirst = true;
break;
}
else if (p == q)//自鏈接節點
return;
else
p = q;
}
// Find active successor
for (Node<E> p = next; ; ++hops) {
if (p.item != null) {
activeSucc = p;
isLast = false;
break;
}
Node<E> q = p.next;
if (q == null) {
if (p.prev == p)
return;
activeSucc = p;
isLast = true;
break;
}
else if (p == q)//自鏈接節點
return;
else
p = q;
}
// TODO: better HOP heuristics
//無節點跳躍并且操作節點有first或last節點時不更新鏈表
if (hops < HOPS
// always squeeze out interior deleted nodes
&& (isFirst | isLast))
return;
// Squeeze out deleted nodes between activePred and
// activeSucc, including x.
//連接兩個活動節點
skipDeletedSuccessors(activePred);
skipDeletedPredecessors(activeSucc);
// Try to gc-unlink, if possible
if ((isFirst | isLast) &&
// Recheck expected state of predecessor and successor
(activePred.next == activeSucc) &&
(activeSucc.prev == activePred) &&
(isFirst ? activePred.prev == null : activePred.item != null) &&
(isLast ? activeSucc.next == null : activeSucc.item != null)) {
updateHead(); // Ensure x is not reachable from head
updateTail(); // Ensure x is not reachable from tail
// Finally, actually gc-unlink
x.lazySetPrev(isFirst ? prevTerminator() : x);
x.lazySetNext(isLast ? nextTerminator() : x);
}
}
}
說明:unlink(Node<E> x)
方法用于解除已彈出節點的鏈接,分三種情況:
- 首先說一下通常的情況(源碼中標注
common case
處),這種情況下,入列和出列非同端操作,即操作節點 x 非 first 和 last 節點, 就執行如下流程:
- 首先找到給定節點 x 的活躍(active)前繼和后繼節點。然后修整它們之間的鏈接,讓它們指向對方(通過
skipDeletedSuccessors
和skipDeletedPredecessors
方法),留下一個從活躍(active)節點不可達的 x 節點(即“unlinking”)。 - 如果成功執行,或者 x 節點沒有 live 的前繼/后繼節點,再嘗試 gc 解除鏈接(gc-unlink),在設置 x 節點的 prev/next 指向它們自己或 TERMINATOR 之前(即“gc-unlink”),需要檢查 x 的前繼和后繼節點的狀態未被改變,并保證 x 節點從 head/tail 不可達(通過
updateHead()
和updateTail()
方法)。
- 如果操作節點為 first 節點(入列和出列都發生在 first 端),則調用
unlinkFirst
解除已刪除節點的鏈接,并鏈接 first 節點到下一個 active 節點(注意,在執行完此方法之后 first 節點是沒有改變的)。unlinkFirst
源碼如下:
/**
* Unlinks non-null first node.
*/
private void unlinkFirst(Node<E> first, Node<E> next) {
// assert first != null;
// assert next != null;
// assert first.item == null;
//從next節點開始向后尋找有效節點,o:已刪除節點(item為null)
for (Node<E> o = null, p = next, q;;) {
if (p.item != null || (q = p.next) == null) {
//跳過已刪除節點,CAS替換first的next節點為一個active節點p
if (o != null && p.prev != p && first.casNext(next, p)) {
//更新p的prev節點
skipDeletedPredecessors(p);
if (first.prev == null &&
(p.next == null || p.item != null) &&
p.prev == first) {
//更新head節點,確保已刪除節點o從head不可達(unlinking)
updateHead(); // Ensure o is not reachable from head
//更新tail節點,確保已刪除節點o從tail不可達(unlinking)
updateTail(); // Ensure o is not reachable from tail
// Finally, actually gc-unlink
//使unlinking節點next指向自身
o.lazySetNext(o);
//設置移除節點的prev為PREV_TERMINATOR
o.lazySetPrev(prevTerminator());
}
}
return;
}
else if (p == q)//自鏈接節點
return;
else {
o = p;
p = q;
}
}
}
- 如果操作節點為 last 節點(入列和出列都發生在 last 端),則調用
unlinkLast
解除已刪除節點的鏈接,并鏈接 last 節點到上一個 active 節點。unlinkLast
與unlinkFirst
方法執行流程一致,只是操作的是 last 端,在此不多贅述。
小結
本章與 ConcurrentLinkedQueue 一篇中的非阻塞算法基本一致,只是為雙端操作定義了幾個可供操作的節點類型。
本章重點:理解 ConcurrentLinkedDeque 的非阻塞算法及節點刪除的三個狀態