Michael-Scott 非阻塞隊列算法中的插入

CAS的基本使用模式:在更新某個值時存在不確定性,以及在更新失敗時重新嘗試。構建非阻塞算法的技巧在於:將執行原子修改的範圍縮小到單個變量上。

  鏈接隊列比棧更爲複雜,因爲它必須支持對頭節點和尾節點的快速訪問。因此,它需要單獨維護的頭指針和尾指針。有兩個指針指向尾部的節點:當前最後一個元素的next指針,以及尾節點。當成功地插入一個新元素時,這兩個指針都需要採用原子操作來更新。

  這裏需要一些技巧來完成,第一個技巧是,即使在一個包含多個步驟的更新操作中,也要確保數據結構總是處於一致的狀態。這樣,當線程B到達時,如果發現線程A正在執行更新,那麼線程B就可以知道有一個操作已部分完成,並且不能立即開始執行自己的更新操作。然後,B可以等待(通過反覆檢查隊列的狀態)並直到A完成更新,從而使兩個線程不會相互干擾。

  雖然這種方法能夠使不同的線程“輪流”訪問呢數據結構,並且不會造成破壞,但如果一個線程在更新操作中失敗了,那麼其他的線程都無法在訪問隊列。要使得該算法成爲一個非阻塞算法,必須確保當一個線程失敗時不會妨礙其他線程繼續執行下去。因此,第二個技巧是,如果當B到達時發現A正在修改數據結構,那麼在數據結構中應該有足夠多的信息,使得B能完成A的更新操作。如果B“幫助”A完成了更新操作,那麼B可以執行自己的操作,而不用等到A的操作完成。當A恢復後再試圖完成其操作時,會發現B已經替它完成了。

  在下面的程序中,給出了 Michael-Scott 提出的非阻塞連界隊列算法中的插入部分,它是由 ConcurrentLinkedQueue 實現的。在許多隊列算法中,空隊列通常都包含一個“哨兵節點”或者“啞(Dummy)節點”,並且頭節點和尾節點在初始化時都指向該哨兵節點。尾節點通常要麼指向哨兵節點(如果隊列爲空),即隊列的最後一個元素,要麼(當有操作正在執行更新時)指向倒數第二個元素。下圖1給出了一個處於正常狀態(或者說穩定狀態)的包含兩個元素的隊列。

Michael-Scott 非阻塞隊列算法中的插入:
複製代碼
 
1 @ThreadSafe
 2 public class LinkedQueue<E> {
 3     private static class Node <E> {
 4         final E item;
 5         final AtomicReference<LinkedQueue.Node<E>> next;
 6 
 7         public Node(E item, LinkedQueue.Node<E> next) {
 8             this.item = item;
 9             this.next = new AtomicReference<LinkedQueue.Node<E>>(next);
10         }
11     }
12 
13     private final LinkedQueue.Node<E> dummy = new LinkedQueue.Node<E>(null, null);
14     private final AtomicReference<LinkedQueue.Node<E>> head
15             = new AtomicReference<LinkedQueue.Node<E>>(dummy);
16     private final AtomicReference<LinkedQueue.Node<E>> tail
17             = new AtomicReference<LinkedQueue.Node<E>>(dummy);
18 
19     public boolean put(E item) {
20         LinkedQueue.Node<E> newNode = new LinkedQueue.Node<E>(item, null);
21         while (true) {
22             LinkedQueue.Node<E> curTail = tail.get();
23             LinkedQueue.Node<E> tailNext = curTail.next.get();
24             if (curTail == tail.get()) {
25                 if (tailNext != null) {  // A
26                     // 隊列處於中間狀態,推進尾節點
27                     tail.compareAndSet(curTail, tailNext); // B
28                 } else {
29                     // 處於穩定狀態,嘗試插入新節點
30                     if (curTail.next.compareAndSet(null, newNode)) { // C
31                         // 插入操作成功,嘗試推進尾節點
32                         tail.compareAndSet(curTail, newNode); // D
33                         return true;
34                     }
35                 }
36             }
37         }
38     }
39 }
複製代碼

 

    圖1 處於穩定狀態幷包含兩個元素的對立

 

  當插入一個新的元素時,需要更新兩個指針。首先更新當前最後一個元素的next 指針,將新節點鏈接到隊列隊尾,然後更新尾節點,將其指向這個新元素。在兩個操作之間,隊列處於一種中間狀態,如圖2。在等二次更新完成後,隊列將再次處於穩定狀態,如圖3所示。

  實現這兩個技巧的關鍵在於:當隊列處於穩定狀態時,尾節點的next域將爲空,如果隊列處於中間狀態,那麼tail.next 將爲非空。因此,任何線程都能夠通過檢查tail.next 來獲取隊列當前的狀態。而且,當隊列處於中間狀態時,可以通過將尾節點移動一個節點,從而結束其他線程正在執行的插入元素操作,並使得隊列恢復爲穩定狀態

      圖2  在插入過程中處於中間狀態的對立

 

    圖3 在插入操作完成後,隊列再次處於穩定狀態

  LinkedQueue.put 方法在插入新元素之前,將首先檢查隊列是否處於中間狀態(步驟A)。如果是,那麼有另一個線程正在插入元素(在步驟C和D之間)。此時當前線程不會等待其他線程執行完成,而是幫助它完成操作,並將尾節點向前推進一個節點(步驟B)。然後,它將重複執行這種檢查,以免另一個線程已經開始插入新元素,並繼續推進尾節點,直到它發現隊列處於穩定狀態之後,纔會開始執行自己的插入操作。

  由於步驟C中的CAS將把新節點鏈接到隊列尾部,因此如果兩個線程同時插入元素,那麼這個CAS將失敗。在這樣的情況下,並不會造成破壞:不會發生任何變化,並且當前的線程只需要重新讀取尾節點並再次重試。如果步驟C成功了,那麼插入操作將生效,第二個CAS(步驟D)被認爲是一個“清理操作”,因爲它既可以由執行插入操作的線程來執行,也可以由其他任何線程來執行。如果步驟D失敗,那麼執行插入操作的線程將返回,而不是重新執行CAS,因爲不再需要重試——另一個線程已經在步驟B中完成了這個工作。

  這種方式能夠工作,因爲在任何線程嘗試將一個新節點插入到隊列之前,都會首先通過檢查tail.next是否非空來判斷是否需要清理隊列。如果是,它首先會推薦尾節點(可能需要執行多次),直到隊列處於穩定狀態。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章