java多線程——CAS

這是多線程系列第五篇,其餘請關注如下:java

java 多線程—線程怎麼來的?node

java多線程-內存模型算法

java多線程——volatile安全

java多線程——鎖多線程

關於無鎖隊列,網上有不少介紹了,我作一個梳理,從它是什麼再到有哪些特性以及應用作一個總結,方便本身記錄和使用。架構

本文主要內容:併發

  1. 非阻塞同步是什麼
  2. CAS是什麼
  3. CAS特性
  4. 無阻塞隊列
  5. ABA問題

 

1、非阻塞同步

 

互斥同步屬於一種悲觀的併發策略,總認爲只要不去作正確的同步措施,確定會出問題,不管共享數據是否真的會出現競爭,它都要進行加鎖。jvm

而基於衝突檢測的樂觀併發策略,是先進行操做,若是沒有競爭,就操做成功了,若是有競爭,產生衝突了,就採用補救措施,常見的就是不斷的重試。性能

CAS就是一種樂觀併發策略,除了CAS之外,還有:測試

  • Test-and-Set(測試並設置),

  • Fetch-and-Increment(獲取並增長),

  • Swap(交換),

  • LL/SC(加載連接/條件存儲)

以上這些都須要硬件指令集的支持才具有原子性。好比在IA6四、x86 CPU架構下能夠經過cmpxchg指令完成CAS功能,而在ARM和PowerPC架構下,須要ldrex/strex指令來完成LL/SC的功能。

 

2、CAS是什麼

 

cas全稱爲Compare-and-Swap(比較並交換),有3個操做數,分別是內存位置V、舊的的預期值A、新值B,那麼當且僅當V符合舊的預期值A時,處理器用新值B更新V的值爲B。而且這些處理過程有指令集的支持,所以看似讀-寫-改操做只是一個原子操做,因此不存在線程安全問題。咱們看個cas的操做過程僞代碼:

int value;

int compareAndSwap(int oldValue,int newValue){



    int old_reg_value =value;

    if (old_reg_value==old_reg_value)

        value=newValue;

    return old_reg_value;

}

 

當多個線程嘗試使用CAS同時更新同一個變量的時候,只有其中一個線程可以更新變量的值。當其餘線程失敗後,不會像獲取鎖同樣被掛起,而是能夠再次嘗試,或者不進行任何操做,這種靈活性就大大減小了鎖活躍性風險。

 

jvm對CAS支持

 

jdk在1.5以前沒有對cas的支持,從jdk1.5開始開始引入了底層的支持,目前在int、long和對象的引用上都公開了cas操做,主要有sum.misc.Unsafe 來包裝,而後由虛擬機對這些方法作特殊處理。在支持cas的平臺上,運行時將其編譯爲相應的機器指令,最壞狀況下,若是不支持cas指令,jvm會使用自旋鎖來代替。

目前java對cas支持的類主要在util.concurrent.atomic 包下面,具體的使用不在介紹了,好比,咱們常見的AtomicInteger就是採用cas操做保證了int值改變的安全性。

 

3、CAS特性

 

咱們知道採用鎖對共享數據進行處理的話,當多個線程競爭的時候,都須要進行加鎖,沒有拿到鎖的線程會被阻塞,以及喚醒,這些都須要用戶態到核心態的轉換,這個代價對阻塞線程來講代價仍是蠻高的,那cas是採用無鎖樂觀方式進行競爭,性能上要比鎖更高些纔是,爲什麼不對鎖競爭方式進行替換?

要回答這個問題,咱們先舉個例子。

當你開車在上班高峯期的時候,若是經過交通訊號燈來控制車流,能夠實現更高的吞吐量,而環島雖然無紅綠燈讓你等待,但你一圈不必定能繞出你先出去的那個路口,有時候可能得多走幾圈,而在低擁堵的時候,環島則能實現更高的吞吐量,你一次就能夠成功,而紅路燈反而效率低下了,即使人很少,你依然須要等待。

這個例子依然適應鎖和cas的比較,在高度競爭的狀況下,鎖的性能將超過cas的性能,但在中低程度的競爭狀況下,cas性能將超過鎖的性能。多數狀況下,資源競爭通常都不會那麼激烈。

 

4、非阻塞無鎖鏈表

 

咱們參考一個ConcurrentLinkedQueue 的源碼實現,來看下cas的應用。

ConcurrentLinkedQueue是一個基於連接節點的無界線程安全隊列,它是個單向鏈表,每一個連接節點都擁有一個當前節點的元素和下一個節點的指針。

 Node<E> {

    volatile E item;

    volatile Node<E> next;

}

它採用先進先出的規則對節點進行排序,當咱們添加一個元素的時候,它會添加到隊列的尾部(tail),當咱們獲取一個元素時,它會返回隊列頭部(head)的元素。tail節點和head節點方便咱們快速定位最後一個和第一個元素。

 

咱們看下添加一個元素的源碼實現:

public boolean offer(E e) {

    checkNotNull(e);

    final Node<E> newNode = new Node<E>(e);

   

   //從tail執向的節點開始循環,查找尾部節點,而後插入,直到插入成功。

    for (Node<E> t = tail, p = t;;) {

        Node<E> q = p.next;

        if (q == null) {

            // p 是最後一個節點

            if (p.casNext(null, newNode)) {

                // 添加下一個節點成功以後,若是當前tail節點的next節點!=null,更新tail的指針指向newNode

                // 若是==null,則不更新t  

                if (p != t)

                    casTail(t, newNode);  // 更新tail節點指針指向newNode

                return true;

            }

            // 沒有競爭上cas的線程,繼續循環

        }

        else if (p == q)

            // 若是next節點指向本身,表示tail指針自引用了,當前只有head一個節點,下一個節點從head開始循環

            p = (t != (t = tail)) ? t : head;

        else

            // 若是tail節點的next節點不爲null,繼續查找尾部節點(尾部節點的next==null)

            p = (p != t && t != (t = tail)) ? t : q;

    }

}

 

上述代碼主要作了以下功能:

一、從tail指針指向的節點開始循環,查找尾節點,尾節點的特徵是next爲null。

二、若是當前節點的nextNode!=null,則繼續查找nextNode的nextNode。

二、若是當前節點的nextNode==null,代表找到尾部節點,則添加newNode到尾部節點的nextNode。

三、更新tail指針,通常指向最新尾節點

四、若是tail節點nextNode==null,則不更新,代表上一次已經指向最新的尾node。

五、若是!=null,則更新爲newNode,2次插入操做更新一次

示圖:

 

上面的代碼算法過於複雜,簡化以下:

while (true){

    //添加尾節點的next節點,成功以後,更新tail的指針指向最新尾節點

    if (tail.casNext(null, newNode)) {

        casTail(tail, newNode); 

        return true;

    }

}

 

爲什麼要2次插入node以後,再更新tail的指針?

一、減小tail的寫入次數,從而減少write開銷

二、tail的讀次數增長不會影響性能,雖然增長一次循環開銷,但相對於寫來講並不大。

三、tail加快入隊效率,不會每次入隊都從head開始找尾部node。

 

有兩次CAS操做,如何保證一致性?

一、若是第一個cas更新成功,第二個失敗,那麼對了tail會出現不一致的狀況。並且即使是都更新成功了,在執行兩個cas之間,仍然可能有另一個線程會訪問這個隊列,那麼如何保證這種多線程狀況下不會出錯。

二、對於第一個問題,即使tail更新失敗,上述代碼也會循環的找到真正的尾節點,在這裏不是強制要求以tail爲尾節點,它只是一個靠近尾節點的指針。

三、第二種狀況,若是線程B抵達時候,發現線程A正在執行更新,那麼B線程會經過反覆循環來檢查隊列的狀態,直到A完成更新,B線程又拿到了nextNode最新信息,添加新的node,從而使兩個線程不會相互干擾。

以上就是ConcurrentLinkedQueue無阻塞鏈表的基本思想,咱們能夠看到如何運用cas來進行共享數據進行更新,以及如何提高效率。源碼採用jdk1.8。

 

5、ABA 問題

 

儘快CAS看起來很完美,但從語義上來講並非完美的,存在這樣一個邏輯漏洞:

若是一個變量V初次讀取的時候是A值,而且在準備賦值的時候檢查到它依然是A值,那麼咱們就認定它沒有改變過。若是在這期間它的值被改成B,後來又改成A,那麼CAS就會誤認爲它歷來沒有被改變過,這個漏洞也被成爲「ABA」問題。

在c的內存管理機制中普遍使用的內存重用機制,若是是cas更新的是指針,機會出現一些指針錯亂的問題。常見的ABA問題解決方式,就是在更新的時候增長一個版本號,每次更新以後版本號+1,從而保證數據一致。

不過大部分狀況下ABA問題都不會影響到程序的正確性,若是須要解決,能夠特殊考慮下,或者採用傳統的互斥同步會更好。

 

-----------------------------------------------------------------------------

想看更多有趣原創的技術文章,掃描關注公衆號。

關注我的成長和遊戲研發,推進國內遊戲社區的成長與進步。

相關文章
相關標籤/搜索