ConcurrentLinkedQueue
ConcurrentLinkedQueue是一個非阻塞的無界隊列。非阻塞和阻塞區別。首先了解一下JAVA中多線程的同步機制基本採用三種方式:
- volatile 輕量級的線程同步,不會引起上下文的切換和線程調度,提供內存的可見性,但不保證原子性。
- CAS 輕量級的線程同步,不會引起上下文的切換和線程調度,提供內存的可見性和原子性。
內部鎖(synchronized)和顯示鎖 重量級的線程同步,可能會引起上下文的切換和下城調度,提供內存的可見性和原子性。
非阻塞算法
一個線程的失敗和掛起不會引起其他些線程的失敗和掛起,這樣的算法稱爲非阻塞算法。非阻塞算法通過使用底層機器級別的原子指令來取代鎖,從而保證數據在併發訪問下的一致性。ConcurrentLinkedQueue就是採用CAS爲基礎來實現非阻塞算法。但是非阻塞算法的設計和實現都非常複雜,這裏只能夠大致學習以及分析一下ConcurrentLinkedQueue的一個實現思路。
我們都知道鏈表是基於節點實現,所以先學習一下ConcurrentLinkedQueue中的節點:
Node
private static class Node<E> {
volatile E item; //節點的域值
volatile Node<E> next; //當前節點的下一個節點
Node(E item) {
UNSAFE.putObject(this, itemOffset, item); //創建新節點。初始化其域值
}
boolean casItem(E cmp, E val) {//用 CAS 指令設置 item 域的值
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
void lazySetNext(Node<E> val) {//惰性設置 next 域的值
UNSAFE.putOrderedObject(this, nextOffset, val);
}
boolean casNext(Node<E> cmp, Node<E> val) {//CAS 設置 next 域的值
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long itemOffset;
private static final long nextOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class k = Node.class;
itemOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("item"));
nextOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("next"));
} catch (Exception e) {
throw new Error(e);
}
}
}
瞭解節點後,先來看一下ConcurrentLinkedQueue的一些基本屬性定義
private transient volatile Node<E> head;
private transient volatile Node<E> tail;
public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null);
}
隊列有一個head節點,tail節點,在初始化一個隊列的時候,head節點和tail節點都是指向一個域值爲空的節點。
offer方法
public boolean offer(E e) {
checkNotNull(e);//檢查元素是否爲空。空則拋出NullPointerException
final Node<E> newNode = new Node<E>(e);//創建一個新節點
for (Node<E> t = tail, p = t;;) {//死循環。定義兩個節點,一個節點指向tail,一個節點指向t
Node<E> q = p.next;
if (q == null) { //說明p是最後一個節點。
if (p.casNext(null, newNode)) { //CAS操作把新節點插入到p節點後
if (p != t)//每插入兩個節點更新一次tail
casTail(t, newNode); // 允許失敗
return true;
}
//CAS失敗,說明有其它線程已經插入,進行下一次循環
}
else if (p == q)
// 遍歷到的p節點已刪除,
// 如果實際tail爲當前局部變量tail,說明tail已在head前,需要跳到head進入正常遍歷;
// 否則,有其他線程維護過tail,從tail開始
p = (t != (t = tail)) ? t : head;
else
// 每隔兩個節點更新局部變量t,向前推進
p = (p != t && t != (t = tail)) ? t : q;
}
}
上面代碼初次理解可能會比較困難。關鍵在於理解tail的位置,它不一定是指向最後一個節點的。tail有三種情況,第一種指向尾節點。一種指向尾節點的前一個節點。還有一種就是指向了一個已經刪除的節點(p==q)一個節點的下一個節點指向自己,就是說明它已經從鏈表中斷開了。
這三種情況對應着上面的三個判斷情況。
poll
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
if (item != null && p.casItem(item, null)) {
// CAS null成功,則說明p已從隊列中刪除
if (p != h) //// 每刪除兩個節點維護一次head
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)// p爲已刪除節點,且已經off-list,重新開始
continue restartFromHead;
else
p = q;
}
}
}