基於CAS操作的非阻塞演算法

位元流發表於2014-01-22
非阻塞演算法(non-blocking algorithms)定義
 
     所謂非阻塞演算法是相對於鎖機制而言的,是指:一個執行緒的失敗或掛起不應該引起另一個執行緒的失敗或掛起的一種演算法。一般是利用硬體層面支援的原子化操作指令來取代鎖的,比如CAS(compare and swap),從而保證共享資料在併發訪問下的資料一致性。
 
由AtomicInteger的執行緒安全機制說起
 
AtomicInteger內部是如何保證執行緒同步的呢?我們先看AtomicInteger內部的一個典型的方法:
     public final int getAndSet(int newValue) {
        for (;;) {
            int current = get();
            if (compareAndSet(current, newValue))
                return current;
        }
   }
     public final boolean compareAndSet (int expect, int update) {
       return unsafe.compareAndSwapInt(thisvalueOffset, expect, update);
   }
 
解釋:compareAndSet方法內部是呼叫Java本地方法compareAndSwapInt來實現的,而compareAndSwapInt方法內部又是藉助C來呼叫CPU的底層指令來保證在硬體層面上實現原子操作的。在intel處理器中,CAS是通過呼叫cmpxchg指令完成的。這就是我們常說的CAS操作(compare and swap)。CAS操作很容易理解,一般來說有三個值:記憶體值V,期望值A,更新值B,如果記憶體值V和期望值A相等,那麼就用更新值B替換記憶體值,否則什麼都不做。想象一下,假設AtomicInteger當前的value值為1,某個執行緒A正在執行上述的getAndSet方法,當執行到compareAndSet方法的時候,被另一個執行緒B搶佔了,執行緒B成功將記憶體值更新為2,然後輪到執行緒A來繼續執行上述還沒有執行完的比較並更新操作,由於執行緒A上次獲得到的current值是1,然後開始執行compareAndSet方法(最後交由CPU的原子執行來執行的),comareAndSet方法發現當前記憶體值V=2,而期望值A=1(current變數值),所以就不會產生值交換,然後繼續下一次重試,在沒有別的執行緒搶佔的情況下,下一個迴圈(在併發很高的情況下可能經過更多次的迴圈)執行緒A就能夠設定成功,如果執行緒A是在還沒有執行int current = get()這一行操作時被搶佔了,那麼執行緒B執行完畢後,執行緒A獲得的將是執行緒B修改後的值然後進行CAS操作可能就一次成功(在沒有其他執行緒搶佔的情況下)。因此,CAS Try-Loop操作能夠很好的提供執行緒同步機制,我們又將此實現過程稱之為執行緒同步的無阻塞演算法,又叫”CAS迴圈”,”lock-free“或”wait-free“演算法。
 
非阻塞演算法的優點
 
  在java5.0版本時,我們只能通過synchronized來實現執行緒的同步,synchronized是一種獨佔鎖,獨佔鎖是一種悲觀鎖,當一個執行緒訪問共享資源的時候,其他執行緒必須處在阻塞狀態,只有在擁有鎖的執行緒釋放鎖以後才能被其他執行緒鎖競爭,JVM 實現阻塞的方式通常是掛起阻塞的執行緒,過一會兒再重新排程它。由此造成的上下文切換相對於鎖保護的少數幾條指令來說,會造成相當大的延遲,這將引起效能問題,所以我們稱這種鎖為重量級鎖。所謂樂觀鎖就是指:對競爭資源不用加鎖,而是假設沒有衝突去完成某項操作,如果因為衝突失敗就不斷重試,直到成功為止。上面所說的迴圈CAS操作就是上述所說的樂觀鎖。
  1. 利用硬體的原生支援代替JVM對程式碼路徑的鎖定,從而提供更細粒度(獨立的記憶體地址)的同步。
  2. 失敗的執行緒可以立即重試而不用被掛起,降低了爭用成本,即使有少量失敗的CAS操作,也依然鎖爭用造成的重新排程快的多
  3. 爭用CAS提供更短的延遲(因為爭用CAS比爭用鎖會更快),提供更好的吞吐率。
  4. 對生存問題(死鎖和執行緒優先順序反轉)提供更好的防禦
在輕度到中度的爭用情況下阻塞演算法的效能會超越阻塞演算法,因為 CAS 的多數時間都在第一次嘗試時就成功,而發生爭用時的開銷也不涉及執行緒掛起和上下文切換,只多了幾個迴圈迭代。沒有爭用的 CAS 要比沒有爭用的鎖便宜得多(這句話肯定是真的,因為沒有爭用的鎖涉及 CAS 加上額外的處理),而爭用的 CAS 比爭用的鎖獲取涉及更短的延遲。在高度爭用的情況下(即有多個執行緒不斷爭用一個記憶體位置的時候),基於鎖的演算法開始提供比非阻塞演算法更好的吞吐率,因為當執行緒阻塞時,它就會停止爭用,耐心地等候輪到自己,從而避免了進一步爭用。但是,這麼高的爭用程度並不常見,因為多數時候,執行緒會把執行緒本地的計算與爭用共享資料的操作分開,從而給其他執行緒使用共享資料的機會。
 
參考:

相關文章