1. 程式人生 > >併發程式設計學習筆記之原子變數與非阻塞同步機制(十二)

併發程式設計學習筆記之原子變數與非阻塞同步機制(十二)

概述

java.util.concurrent包中的許多類,比如Semaphore和ConcurrentLinkedQueue,都提供了比使用Synchronized更好的效能和可伸縮性.這是因為它們的內部實現使用了原子變數和非阻塞的同步機制.

近年來很多關於併發演算法的研究都聚焦在非阻塞演算法(nonblocking algorithms),這種演算法使用低層原子化的機器指令取代鎖,比如比較並交換(compare-and-swap),從而保證資料在併發訪問下的一致性.非阻塞演算法廣泛應用於作業系統和JVM中的執行緒和程序排程、垃圾回收以及實現鎖和其他的併發資料結構.

與基於鎖的方案相比,非阻塞演算法的設計和實現都要複雜得多,但是他們在可伸縮性和活躍度上佔有很大的優勢

.

因為非阻塞演算法可以讓多個執行緒在競爭相同資源時不會發生阻塞,所以它能在更精化的層面上調整粒度,並能大大減少排程的開銷.

進一步而言,它們對死鎖和其它活躍度問題具有免疫性.在基於鎖的演算法中,如果一個執行緒在持有鎖的時候休眠,或者停滯不前,那麼其他執行緒就都不可能前進了,而非阻塞演算法不會受到單個執行緒失敗的影響.

在Java 5.0中,使用原子變數(atomic variable classes),比如AtomicInteger和AtomicReference,能夠高效地構建非阻塞演算法.

即使你不使用原子變數開發阻塞演算法,它也可以當做更優的volatile變數來使用.原子變數提供了與volatile型別變數相同的記憶體語義,同時還支援原子更新

--使它們能更加理想地用於計數器、序列發生器和統計資料收集等,另外也比基於鎖的方案具有更加出色的可伸縮性.

1. 鎖的劣勢

使用一致的加鎖協議來協調對共享狀態的訪問,確保無論哪個執行緒持有守護變數的鎖,他們都能獨佔地訪問這些變數,並且對變數的任何修改對其他隨後獲得同一鎖的執行緒都是可見的.

現代JVM能夠對非競爭鎖的獲取和釋放進行優化,讓它們非常高效,但是如果有多個執行緒同時請求鎖JVM就需要向作業系統尋求幫助.

倘若出現了這種情況,一些"不幸的"執行緒將會掛起,稍後恢復執行.從執行緒開始恢復起,到它真正被呼叫前,可能必須等待其它執行緒完成它們的排程限額規定的時間.掛起和恢復執行緒會帶來很大的開銷

,並通常伴有冗長的中斷.

對於基於鎖,並且其操作過度細分的類(比如同步容器類,大多數方法只包含很少的操作),當頻繁地發生鎖的競爭時,排程與真正用於工作的開銷間的比值會很可觀.

volatile變數與鎖相比是更輕量的同步機制,因為它們不會引起上下文的切換和執行緒排程.

然而,volatile變數與鎖相比有一些侷限性:儘管他們提供了相似的可見性保證,但是它們不能用於構建原子化的複合操作.

這意味著當一個變數依賴其他變數時,或者當變數的新值依賴於舊值時,是不能用volatile變數的.這些都限制了volatile變數的使用,因此它們不能用於實現可靠的通用工具,比如計數器.

儘管自增操作(++i)看起來像是原子操作,事實上有3個獨立操作--獲取變數當前值,加1,然後寫會更新值.為了不丟失更新,整個讀-改-寫操作必須是原子的.可以使用加鎖保證它的執行緒安全.

在幾乎沒有競爭的條件下會執行良好.但是在競爭下,效能會由於上下文切換的開銷和排程延遲而受到損失.倘若臨時佔有鎖的執行緒進入休眠,將成為又一個問題,因為會有執行緒在這個錯誤的時間裡請求鎖.

加鎖還有其他的缺點.當一個執行緒正在等待鎖時,它不能做任何其他事情.如果一個執行緒在持有鎖的情況下發生了延遲(原因包括頁錯誤、排程延遲、或者類似情況),那麼其他所有需要該鎖的執行緒都不能前進了.

如果阻塞的執行緒是優先順序很高的執行緒,持有鎖的執行緒優先順序較低,那麼會造成嚴重問題--效能風險,被稱為優先順序倒置(priority inversion).

即使更高的優先順序佔先,它仍然需要等待鎖被釋放,這導致它的優先順序會降至與優先順序較低的執行緒相同的水平.

如果持有鎖的執行緒發生了永久性的阻塞(因為無限迴圈、死鎖、活鎖和其它活躍度失敗),所有等待該鎖的執行緒都不會前進了.

即使忽略這樣的風險,加鎖對於細分的操作而言,仍是重量級(heavyweight)的機制,比如遞增計數器.本應該有更好的技術用來管理執行緒之間的競爭--某些類似於volatile變數的機制,但是還要支援原子化更新.幸運的是,現代處理器為我們提供了這樣的機制.

2. 硬體對併發的支援

獨佔鎖是一項悲觀的技術--它假設最壞的情況(總會有其他執行緒去修改變數),並且會通過獲得正確的鎖來避免其他執行緒的打擾,直到做出保證才能繼續進行.

對於細粒度的操作,有另外一種選擇通常更加有效--樂觀的解決方法.憑藉新的方法,我們可以指望不受打擾地完成更新.這個方法依賴於衝突檢測,從而能判定更新過程中是否存在來自其他成員的干涉,在衝突發生的情況下,操作失敗,並會重試(也可能不重試).這種方式更有效率.

如今,幾乎所有現代的處理器都具有一些形式的原子化的讀-改-寫指令,比如比較並交換(compare-and-swap)和載入連結/儲存條件(load-linked/store-conditional).作業系統和JVM使用這些指令來實現鎖和併發的資料結構.

2.1 比較並交換

大多數處理器使用的架構方案都實現了比較並交換(CAS)指令(其他處理器,使用一對指令實現了相同的功能:連結載入/儲存條件).

CAS有3個運算元---記憶體位置V、舊的預期值A和新值B.當且僅當V符合舊預期值A時,CAS用新值B原子化的更新V的值;否則它什麼都不會做.在任何一種情況下,都會返回V的真實值.(這個變數稱為compare-and-set,無論操作是否成功都會返回).

CAS的意思是:"我認為V的值是A,如果是,那麼將B的值賦給V,若不是,則不修改,無論如何都返回V的值"

CAS是一項樂觀技術--它抱著成功的希望進行更新,並且如果另一個執行緒在上次檢查後更新了該變數,它能夠發現錯誤.

闡釋CAS的語意(並非真正的實現或執行方式):

public class SimulatedCAS {

    private int value;

    //原子的操作
    public synchronized int getValue(){
        return value;
    }

    //原子的操作
    public synchronized int compareAndSwap(int expectedValue,int newValue){
        //到達快取中的值
        int oldValue = value;
        //如果快取中的值等於傳入來的期望的舊值
        if(oldValue == expectedValue){
            //把新值賦給記憶體中的值
            value = newValue;
        }
        //返回的是第一次得到的記憶體中的值
        return oldValue;
    }

    //原子的操作
    public synchronized boolean compareAndSet(int expectedValue,int newValue){
        //如果兩個值相等,返回true並設定新的value值,否則返回false
        return (expectedValue == compareAndSwap(expectedValue,newValue));
    }


}

使用CAS的時候,必須傳入和記憶體中的值相同的舊值,才能將記憶體中的值更新為新值.

當多個執行緒試圖使用CAS同時更新相同的變數時,其中一個會勝出,並更新變數的值,而其他的都會失敗.失敗的執行緒不會被掛起(但是在使用鎖的情況下,沒有獲取鎖的執行緒就會被掛起);它們會被告知這一次賽跑失利,但允許再嘗試.

因為一個執行緒在競爭CAS時失敗不會被阻塞,它可以決定是否重試,進行一些補救行動,或者什麼都不做(最好的處理方式,可能其他執行緒已經完成了你要做的事情).這樣的靈活性大大減少了與鎖相關的活躍度風險(可能在極端的情況下引入活鎖風險).

2.2 非阻塞計數器

public class CasCounter {

    private SimulatedCAS value = new SimulatedCAS();

    public int getValue(){
        return value.get();
    }

    public int increment(){
        int v ;
       
        do{
            v = value.get();
        } while (v != value.compareAndSwap(v,v+1));

        return v+ 1;
    }
    
    public static void main(String [] args){
                CasCounter casCounter = new CasCounter();
        for (int i = 0; i < 10; i++) {
            System.out.println("casCounter.increment() = " + casCounter.increment());
        }

    }  
}

自增操作遵循了經典形式--取得舊值,根據它計算出新值(加1),並使用CAS設定新值.

如果CAS失敗,立即重試操作.儘管在競爭十分激烈的情況下,更希望等待或者回退,以避免重試造成的活鎖,但是,通常反覆重試都是合理的策略.

CasCounter不會發生阻塞,如果其他執行緒同時更新計數器,他會進行數次重試.實踐中,如果你僅僅需要一個計數器,獲取序列生成器,直接使用AtomicInteger或者AtomicLong就可以了,它們提供原子化的自增方法和其他算數方法.

初看起來,基於CAS的計數器看起來比基於鎖的計數器效能差一些:它具有更多的操作和更復雜的控制流,表面看來還依賴於複雜的CAS的操作.但是,實際上基於CAS的計數器,效能上遠遠勝過了基於鎖的計數器,哪怕只有很小的競爭,或者不存在競爭.

獲取非競爭鎖的快速路徑,通常至少需要一個CAS加上一個鎖相關的細節瑣事,所以,基於鎖的計數器的最好情況也要比基於CAS的一般情況做更多的事情.

因為CAS在大多數情況下都能成功(假設競爭程度中等偏下),硬體將能夠正確地預知隱藏在while迴圈中的分支,使本該更復雜的控制邏輯的開銷最小化.

加鎖的語法可能比較簡潔,但是JVM和OS管理鎖的工作卻並不簡單.加鎖需要遍歷JVM中整個複雜的程式碼路徑,並可能引起系統級的加鎖、執行緒掛起以及上下文切換.在最優的情況下,加鎖需要至少一個CAS,所以使用鎖時把CAS拋在一邊幾乎不能節省任何真正的開銷.

另一方面,程式內部執行CAS不會呼叫到JVM的程式碼、系統呼叫或者排程活動.在應用級看起來越長的程式碼路徑,在考慮到JVM和OS的時候,事實上會變成更短的程式碼.

CAS最大的缺點:它強迫呼叫者處理競爭(通過重試、回退,或者放棄);然而在鎖被獲得之前,卻可以通過阻塞自動處理競爭(CAS最大的缺陷在於難以正確地構建外圍演算法).

即使是在"快速路徑"上,獲取和釋放無競爭鎖的開銷大約也是CAS的兩倍.

3. 原子變數類

原子變數比鎖更精巧、更輕量,並且在多處理器系統中,對實現高效能的併發程式碼非常關鍵.

實現原子變數的快速(非競爭)路徑,並不會比獲取鎖的快速路徑差,並且通常會更快;而慢速路徑絕對比鎖的慢速路徑快,因為它不會引起執行緒的掛起和重新排程.

在使用原子變數取代鎖的演算法中,執行緒更不易出現延遲,如果它們遇到競爭,也更容易恢復.

原子變數類,提供了廣義的volatile變數,以支援原子的、條件的讀-寫-該操作.

AtomicInteger代表了一個int值,並提供了get和set方法,它們與讀取和寫入可變的int有著相同的記憶體語義.

它同樣提供了一個compareAndSet方法(如果該方法成功成功,其記憶體效果和同時讀取寫入一個volatile變數是一樣的),以及原子化的插入、遞增、遞減等方法,這樣是為了使用方便.

AtomicInteger與擴充套件的Counter表面上看有共同之處,但是在競爭條件下AtomicInteger提供了更好的可伸縮性,因為它直接利用硬體對併發的支援.

原子變數類有12個,分成4組:計數器、域更新器(field updater)、陣列以及複合變數.

最常用的原子變數是計數器:AtomicInteger、AtomicLong、AtomicBoolean以及AtomicReference.它們都支援CAS:AtomicInteger和AtomicLong還支援算術運算.

原子化的陣列類(只有 Integer、Long和Reference版本的可用),它的元素是可以被原子化地更新的.

原子陣列類為陣列的元素提供了volatile的訪問語義,這是普通陣列所沒有的特性---volatile型別的陣列只針對陣列的引用具有volatile語義,而不是它的元素.

基本型別的包裝類(Integer/Long)是不可變的,而原子變數類是可變的.

3.1 效能比較: 鎖與原子變數

image

image

在激烈競爭下,鎖勝過原子變數,但是在沒那麼激烈的條件下,原子變數會勝過鎖.這是因為鎖通過掛起執行緒來響應競爭,減少了CPU的利用和共享記憶體總線上的同步通訊量.(這與在生產者-消費者執行緒中,以阻塞生產者來減小消費者負荷,使消費者能夠趕上進度的設計是類似的.)

使用原子變數把競爭推回給呼叫類.通過不斷反覆嘗試來響應競爭,通常是正確的,但是在激烈競爭環境下,會帶來更多競爭.

在實踐中,原子化的伸縮性比鎖更好,因為在典型的競爭級別中,原子性會帶來更好的效率.

鎖與原子化隨競爭度的不同,效能發生的改變闡明瞭各自的優勢和劣勢.在中低程度的競爭下,原子化提供更好的伸縮性;在高強度的競爭下,鎖能夠更好地幫助我們避免競爭.

4. 非阻塞演算法

基於鎖的演算法會帶來一些活躍度的風險. 如果執行緒在持有鎖的時候因為阻塞I/O,頁面錯誤,或其他原因發生延遲,很可能所有執行緒都不能前進了.

一個執行緒的失敗或掛起不應該影響其他執行緒的失敗或掛起,這樣的演算法被稱為非阻塞(nonblocking)演算法.

如果演算法的每一步驟中都有一些執行緒能夠繼續執行,那麼這樣的演算法稱為鎖自由(lock-free)演算法.

線上程間使用CAS進行協調,這樣的演算法如果能構建正確的話,它既是非阻塞的,又是鎖自由的.

非競爭的CAS總是能夠成功,如果多個執行緒以一個CAS競爭,總會有一個勝出並前進.

非阻塞演算法對死鎖和優先順序倒置有"免疫性"(但它們可能會出現飢餓和活鎖,因為他們允許重進入).有關死鎖、活鎖和飢餓的相關知識,請移駕併發程式設計學習筆記之死鎖(八).

4.1 非阻塞棧

棧是最簡單的鏈式資料結構:每個元素僅僅引用唯一的元素,並且每個元素只被一個元素引用.(看下面程式碼,這裡面只儲存一個最新的Node,但是這個Node中有一個相連的Node引用,可以通過Node.next.next.next如此反覆得到所有的Node)

public class ConcurrentStack<E> {

    private static class Node<E>{
        public final E item;
        public Node<E> next;

        public Node(E item){
            this.item = item;
        }
    }


    //這裡面存放的是一個Node,但是通過Node.next.next.next可以拿到所有Node
    AtomicReference<Node<E>> top = new AtomicReference<Node<E>>();

    public void push(E item){
        Node<E> newHead = new Node<E>(item);
        Node<E> oldHead;
        do{
            oldHead = top.get();
            /*
             *  這是一個連結串列, 當有新值加入,會把舊值掛在新值的next方法上,
             *  如此反覆,可以通過遞迴next拿到所有Node
             * */
            newHead.next = oldHead;
        }
        /*如果oldHead和記憶體中的舊值相同,更新為newHead,返回設定的結果
        * 如果設定成功設定新值返回true,跳出迴圈
        * */
        while (!top.compareAndSet(oldHead,newHead));
    }


    /*移除一個最新值*/
    public E pop(){
        Node<E> oldHead;
        Node<E> newHead;
        do{
            //得到當前最新值
            oldHead = top.get();
            if(oldHead == null){
                return null;
            }
            //得到第二新的值
            newHead = oldHead.next;
        }
        /*使第二新的值替換舊的值,如果成功退出迴圈*/
        while (!top.compareAndSet(oldHead,newHead));
        //返回移出的新的值
        return oldHead.item;
    }



}

在併發訪問的環境下,push和pop方法通過top.compareAndSet方法可以保證棧的原子性和可見性,從而安全高效的更新非阻塞棧.

CasCounter和ConcurrentStack闡釋了非阻塞演算法的所有特性:一些任務的完成具有不確定性,並可能重做.在ConcurrentStack中,當我們構建表示新節點的Node時,我們希望next的引用值(通過top.get獲得的)在該節點裝入棧的時候仍然有效,但是同時仍然為重試做好了準備.

在ConcurrentStack等中用到的非阻塞演算法,其執行緒安全性源於:compareAndSet既能提供原子性,又能提供可見性保證,加鎖也可以保證.

當一個執行緒改變棧的狀態時,它使用具有與寫入volatile變數相同的記憶體效應的compareAndSet.當執行緒檢查棧的時候,通過呼叫同一個AtomicReference的get方法來實現,它具有與讀取volatile變數相同的記憶體效應.所以任何執行緒修改都能夠安全地釋出給其他正在檢查列表狀態的執行緒.並且這個列表通過compareAndSet進行修改,更新top的引用或者因發現其它執行緒的干擾而失敗,這些都是原子化地進行的.

4.2 非阻塞連結串列

到目前為止我們已經見到了兩個非阻塞演算法,即計數器和棧,他們闡釋了使用CAS"投機地"更新值這以最基本的模式,如果更新失敗就重試.

構建非阻塞演算法的竅門是:縮小原子化的範圍到唯一的變數.

使用非阻塞演算法實現一個連結佇列比棧更復雜.因為它需要支援首尾的快速訪問.需要維護兩個獨立的隊首指標和隊尾指標,初始時都指向佇列的末尾節點.在成功加入新元素時兩個指標都需要原子化的更新.

使用連結佇列構建非阻塞演算法需要考慮兩種情況:

  1. 第一個指標位置更新成功,第二個失敗,佇列將處於不一致的境地.
  2. 兩個都成功了,另一個操作也可能在兩個更新操作之間來訪問佇列.

要完成這個任務需要幾個竅門:

  1. 即使在多步更新中,也要確保資料結構總能處於一致狀態.這樣執行緒B在到達時發現執行緒A正在更新,B可以分辨操作已部分完成,並且知道不能立即開始自己的更新.這樣B就開始等待(通過反覆檢查佇列狀態)直到A完成更新,這樣兩個執行緒就不會相互影響了.

  2. 確保如果B到達時發現數據結構正在被A修改,在資料結構中應該有足夠多的資訊,說明需要B來替代A完成更新.如果B"幫助"A完成其操作,那麼B可以進行自己的操作,而不用等待A的操作完成,當A恢復後試圖完成其操作,會發現B已經替它完成了.

Michael-Scott的非阻塞連結佇列演算法的插入部分,已經用到了ConcurrentLinkedQueue中:

public class LinkedQueue<E> {

    private static class Node<E>{
        final E item;
        //atomicReference 是非阻塞演算法,可以保證放入元素的原子性和可見性.
        final AtomicReference<Node<E>> next;

        /*
        * 通過構造方法給item和next賦值
        * */
        public Node(E item, Node<E> next) {
            this.item = item;
            this.next = new AtomicReference<Node<E>>(next);
        }

        @Override
        public String toString() {
            return "Node{" +
                    "item=" + item +
                    ", next=" + next +
                    '}';
        }
    }

    //初始化一個dummy,賦值兩個null
    private final Node<E> dummy = new Node<E>(null,null);

    /*
    * 宣告一個AtomicReference型別的head,把dummy放進去,
    * 可以保證dummy的原子性和可見性
    * */
    private final AtomicReference<Node<E>> head =
            new AtomicReference<Node<E>>(dummy);

    //同上
    private final AtomicReference<Node<E>> tail =
            new AtomicReference<Node<E>>(dummy);

    /*
    * 新增元素,返回一個Boolean型別的值.
    * */
    public boolean put(E item){
        //宣告一個新的節點,將item放進去
        Node<E> newNode = new Node<E>(item,null);

        //建立一個死迴圈
        while (true){
            //得到當前尾部的值
            Node<E> curTail = tail.get();
            //得到當前尾部的值的下一個值
            Node<E> tailNext = curTail.next.get();
            /*
            * 如果當前尾部的值 == 最新一次取到的尾部值,
            * 往下走, 否則進入下一次迴圈.
            * 這裡保證不會有過期值.
            * */
            if(curTail == tail.get()){
                /*
                *步驟A, 如果當前尾部值的下一個值不為null,
                * 也就是不是第一次往裡放值的情況
                * */
                if(tailNext != null){
                    // 步驟B,佇列處於靜止狀態,推進尾節點,進入下次迴圈

                    tail.compareAndSet(curTail,tailNext);
                }else{
                    //步驟C,處於靜止狀態,嘗試插入新節點
                    if(curTail.next.compareAndSet(null,newNode)){
                        //步驟D
                        tail.compareAndSet(curTail,newNode);
                        return true;
                    }
                }
            }
        }
    }

    @Override
    public String toString() {
        return "LinkedQueue{" +
                "dummy=" + dummy +
                ", head=" + head +
                ", tail=" + tail +
                '}';
    }

    public static void main(String [] args){
                LinkedQueue<String> linkedQueue = new LinkedQueue<>();
                linkedQueue.put("a");
                linkedQueue.put("b");
                linkedQueue.put("c");
                //linkedQueue.put("b");
        System.out.println("linkedQueue = " + linkedQueue);
    }



}

列印輸出:
linkedQueue = LinkedQueue{dummy=Node{item=null, next=Node{item=a, next=Node{item=b, next=Node{item=c, next=null}}}}, head=Node{item=null, next=Node{item=a, next=Node{item=b, next=Node{item=c, next=null}}}}, tail=Node{item=c, next=null}}

一個空佇列有一個"哨兵(sentinel)節點"或者"虛(dummy)節點",並且隊首指標和隊尾指標的初始化都指向哨兵節點.隊尾指標永遠指向哨兵節點,也就是佇列的最後一個元素(看列印輸出的tail就可以看出,永遠輸出的是最後一個新增的元素的節點),或者(當操作正在更新佇列時)指向倒數第二個元素.

要想同時實現兩個竅門的方法是:假設佇列處於穩定狀態,則tail節點的next域的值為null,如果佇列處於中間狀態tail.next為非空.所以任何執行緒都能夠通過檢查tail.next即時地瞭解佇列狀態.進一步而言,如果佇列處於中間狀態,它能夠通過推進tail指標向前移動一個節點把狀態恢復為穩定(tail.compareAndSet(curTail,tailNext)這一行),結束任意執行緒正在插入元素的操作.

LinkedQueue.put在嘗試插入前首先檢查是否處於中間狀態(步驟A).如果是,那麼其他執行緒正在插入元素(在步驟C和D之間).除了等待執行緒完成,當前執行緒還能夠幫助結束操作,推進隊尾指標(步驟B).在這之後,如果另一個執行緒已經開始插入新元素了,它會進行重複檢查,繼續推進隊尾指標直到它發現佇列處於穩定狀態,可以開始自己的插入了.

步驟C的CAS,在隊尾連結了新的節點,如果兩個執行緒檢視同時插入元素,會造成失敗.在這樣的情況下不會造成危害:沒有發生改變,當前的執行緒只需要過載隊尾指標並重試.C一旦成功,插入就生效了.

第二個CAS(步驟D)被用作"清除",因為它可以由插入的執行緒執行,也可以由其他任何執行緒.如果D失敗,插入執行緒無論如何都會返回,而不是重新嘗試CAS,因為不再需要重試了---另一個執行緒已經在步驟B就完成了這個工作!這樣做不會有問題,因為在所有執行緒嘗試向佇列連結新節點之前,首先通過檢查tail.next是否為空來判定佇列是否需要清理,如果是,它首先會推進隊尾指標(可能多次),直到佇列處於穩定狀態(tail.next為空).

4.3 原子化的域更新器

之前的程式碼說的只是一個實現的大概意思,ConcurrentLinkedQueue的真實實現與它略有區別.

ConcurrentLinkedQueue並未使用原子化的引用,而是使用普通的volatile引用來代表每一個節點,並通過基於反射的AtomicReferenceFieldUpdater來進行更新.


    private static class Node<E>{
        final E item;
        volatile Node<E> next;


        public Node(E item) {
            this.item = item;
        }

    }

    private static AtomicReferenceFieldUpdater<Node,Node> nextUpdater =
            AtomicReferenceFieldUpdater.newUpdater(Node.class,Node.class,"next");

原子化的域更新器類,代表著已存在的volatile域基於反射的"檢視",使得CAS能夠用於已有的volatile域.

域更新器類並不依賴特定的例項;它可以用於更新目標類任何例項的目標域.

更新器類提供的原子性保護比普通的原子類差一些,因為你不能保證底層的域不被直接修改.--compareAndSet和算數方法只在其它執行緒使用原子化的域更新器方法時保證其原子性.

在ConcurrentLinkedQueue中,更新Node的next域是通過使用nextUpdater的compareAndSet方法實現的.這是出於效能的考慮.對於頻繁分配的、生命週期短暫的物件,比如佇列的連結節點,減少每個Node的AtommicReference建立,對於減少插入操作的開銷是非常有效的.

原子變數其實已經夠好了,盡在很少的情況下才需要原子化的域更新器(當你想要儲存現有類的序列化形式時,原子化的域更新器會非常有用).

4.4 ABA問題

在CAS,記憶體中的值V如果和A相同就把V更新為B,但是 如果V的值先改為了C,又改回了A,也是可以更新的,但是中間有一個值改變又改變回來的過程. 這就是ABA問題

如果需要知道值改變過,有一種簡單的解決方案:更新一對值,包括引用和版本號,而不是僅更新值的引用.

即使值由A改為B,又改回A,版本號也是不同的.

AtomicStampedReference以及它的同系AtomicMarkableReference提供了一堆變數原子化的更新條件.更新物件引用的證書對,允許"版本化"引用是能夠避免ABA問題的.

總結

非阻塞演算法通過使用低層級併發原語,比如比較並交換,取代了鎖.原子變數類向用戶提供了這些低層級原語,也能夠當做"更佳的volatile變數"使用,同時提供了整數類和物件引用的原子化更新操作.

非阻塞演算法在設計和實現中很困難,但是在典型條件下能夠提供更好的可伸縮性,並能更好地預防活躍度失敗.從JVM的一個版本到下一個版本間併發性的提升很大程度上來源於非阻塞演算法的使用,包括在JVM內部以及平臺類庫.