無鎖算法CAS 概述


無鎖算法CAS 概述 

  JDK5.0以后的版本都引入了高級並發特性,大多數的特性在java.util.concurrent包中,是專門用於多線並發編程的,充分利用了現代多處理器和多核心系統的功能以編寫大規模並發應用程序。主要包含原子量、並發集合、同步器、可重入鎖,並對線程池的構造提供了強力的支持。


  原子量是定義了支持對單一變量執行原子操作的類。所有類都有get和set方法,工作方法和對volatile變量的讀取和寫入一樣。並發集合是原有集合框架的補充,為多線程並發程序提供了支持。主要有:BlockingQueue,ConcurrentMap,ConcurrentNavigableMap。

 

  同步器提供了一些幫助在線程間協調的類,包括semaphores,barriers,latches, exchangers等。


  一般同步代碼依靠內部鎖(隱式鎖),這種鎖易於使用,但是有很多局限性。新的Lock對象支持更加復雜的鎖定語法。和隱式鎖類似,每一時刻只有一個線程能夠擁有Lock對象,通過與其相關聯的Condition對象,Lock對象也支持wait和notify機制。

 

  線程完成的任務(Runnable對象)和線程對象(Thread)之間緊密相連。適用於小型程序,在大型應用程序中,把線程管理和創建工作與應用程序的其余部分分離開更有意義。線程池封裝線程管理和創建線程對象。 

 

1.原子量

  近來關於並發算法的研究主要焦點是無鎖算法(nonblocking algorithms),這些無鎖算法使用低層原子化的機器指令,例如使用compare-and-swap(CAS)代替鎖保證並發情況下數據的完整性。無鎖算法廣泛應用於操作系統與JVM中,比如線程和進程的調度、垃圾收集、實現鎖和其他並發數據結構。

  在 JDK5.0 之前,如果不使用本機代碼,就不能用 Java 語言編寫無等待、無鎖定的算法。在 java.util.concurrent 中添加原子變量類之后,這種情況發生了變化。本節了解這些新類開發高度可伸縮的無阻塞算法。

  要使用多處理器系統的功能,通常需要使用多線程構造應用程序。但是正如任何編寫並發應用程序的人可以告訴你的那樣,要獲得好的硬件利用率,只是簡單地在多個線程中分割工作是不夠的,還必須確保線程確實大部分時間都在工作,而不是在等待更多的工作,或等待鎖定共享數據結構。

  如果線程之間不需要協調,那么幾乎沒有任務可以真正地並行。以線程池為例,其中執行的任務通常相互獨立。如果線程池利用公共工作隊列,則從工作隊列中刪除元素或向工作隊列添加元素的過程必須是線程安全的,並且這意味着要協調對頭、尾或節點間鏈接指針所進行的訪問。正是這種協調導致了所有問題。

2.鎖同步法

  在 Java 語言中,協調對共享字段訪問的傳統方法是使用同步,確保完成對共享字段的所有訪問,同時具有適當的鎖定。通過同步,可以確定(假設類編寫正確)具有保護一組訪問變量的所有線程都將擁有對這些變量的獨占訪問權,並且以后其他線程獲得該鎖定時,將可以看到對這些變量進行的更改。弊端是如果鎖定競爭太厲害(線程常常在其他線程具有鎖定時要求獲得該鎖定),會損害吞吐量,因為競爭的同步非常昂貴。對於現代 JVM 而言,無競爭的同步現在非常便宜。

  基於鎖的算法的另一個問題是:如果延遲具有鎖的線程(因為頁面錯誤、計划延遲或其他意料之外的延遲),則沒有要求獲的鎖的線程可以繼續運行。

  還可以使用volatile變量來以比同步更低的成本存儲共享變量,但它們有局限性。雖然可以保證其他變量可以立即看到對volatile變量的寫入,但無法呈現原子操作的讀-修改-寫順序,這意味着volatile變量無法用來可靠地實現互斥(互斥鎖定)或計數器。

  下面以實現一個計數器為例。通常情況下一個計數器要保證計數器的增加,減少等操作需要保持原子性,使類成為線程安全的類,從而確保沒有任何更新信息丟失,所有線程都看到計數器的最新值。使用內部鎖實現的同步代碼一般如下:

  

package snippet;

public class SynchronizedCounter {
    private int value;

    public synchronized int getValue() {
        return value;
    }

    public synchronized int increment() {
        return ++value;
    }

    public synchronized int decrement() {
        return --value;
    }
}

 

  increment() 和 decrement() 操作是原子的讀-修改-寫操作,為了安全實現計數器,必須使用當前值,並為其添加一個值,或寫出新值,所有這些均視為一項操作,其他線程不能打斷它。否則,如果兩個線程試圖同時執行增加,操作的不幸交叉將導致計數器只被實現了一次,而不是被實現兩次。(注意,通過使值變量成為volatile變量並不能可靠地完成這項操作。)

  計數器類可以可靠地工作,在競爭很小或沒有競爭時都可以很好地執行。然而,在競爭激烈時,這將大大損害性能,因為JVM用了更多的時間來調度線程,管理競爭和等待線程隊列,而實際工作(如增加計數器)的時間卻很少。

  使用鎖,如果一個線程試圖獲取其他線程已經具有的鎖,那么該線程將被阻塞,直到該鎖可用。此方法具有一些明顯的缺點,其中包括當線程被阻塞來等待鎖時,它無法進行其他任何操作。如果阻塞的線程是高優先級的任務,那么該方案可能造成非常不好的結果(稱為優先級倒置的危險)。

  使用鎖還有一些其他危險,如死鎖(當以不一致的順序獲得多個鎖時會發生死鎖)。甚至沒有這種危險,鎖也僅是相對的粗粒度協調機制,同樣非常適合管理簡單操作,如增加計數器或更新互斥擁有者。如果有更細粒度的機制來可靠管理對單獨變量的並發更新,則會更好一些;在大多數現代處理器都有這種機制。

3.比較並交換  

  大多數現代處理器都包含對多處理的支持。當然這種支持包括多處理器可以共享外部設備和主內存,同時它通常還包括對指令系統的增加來支持多處理的特殊要求。特別是,幾乎每個現代處理器都有通過可以檢測或阻止其他處理器的並發訪問的方式來更新共享變量的指令

  現在的處理器(包括 Intel 和 Sparc 處理器)使用的最通用的方法是實現名為“比較並交換(Compare And Swap)”或 CAS 的原語。(在 Intel 處理器中,比較並交換通過cmpxchg 系列指令實現。PowerPC 處理器有一對名為“加載並保留”和“條件存儲”的指令,它們實現相同的目地;MIPS 與 PowerPC 處理器相似,除了第一個指令稱為“加載鏈接”。

  CAS 操作包含三個操作數—— 內存位置(V)、預期原值(A)和新值(B)。如果內存位置的值與預期原值相匹配,那么處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”

  通常將 CAS 用於同步的方式是從地址 V 讀取值 A,執行多步計算來獲得新值 B,然后使用 CAS 將 V 的值從 A 改為 B。如果 V 處的值尚未同時更改,則 CAS 操作成功。

  類似於 CAS 的指令允許算法執行讀-修改-寫操作,而無需害怕其他線程同時修改變量,因為如果其他線程修改變量,那么 CAS 會檢測它(並失敗),算法可以對該操作重新計算。下面的程序說明了 CAS 操作的行為(而不是性能特征),但是 CAS 的價值是它可以在硬件中實現,並且是極輕量級的(在大多數處理器中)。后面我們分析Java的源代碼可以知道,JDK在實現的時候使用了本地代碼。下面的代碼說明CAS的工作原理(為了便於說明,用同步語法表示)。

  

public class SimulatedCAS {
    private int value;

    public synchronized int getValue() {
        return value;
    }

    public synchronized int compareAndSwap(int expectedValue, int newValue) {
        if (value == expectedValue)
            value = newValue;
        return value;
    }
}

  基於 CAS 的並發算法稱為“無鎖定算法”,因為線程不必再等待鎖定(有時稱為互斥或關鍵部分,這取決於線程平台的術語)。無論 CAS 操作成功還是失敗,在任何一種情況中,它都在可預知的時間內完成。如果 CAS 失敗,調用者可以重試 CAS 操作或采取其他適合的操作。下面的代碼顯示了重新編寫的計數器類來使用 CAS 替代鎖定:

class CasCounter {
    private SimulatedCAS value;

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

    public int decrement() {
        int oldValue = value.getValue();
        while (value.compareAndSwap(oldValue, oldValue + 1) != oldValue)
            oldValue = value.getValue();
        return oldValue + 1;
    }
}

  如果每個線程在其他線程任意延遲(或甚至失敗)時都將持續進行操作,就可以說該算法是"無等待" 的. "無鎖定算法" 要求某個線程總是執行操作. (無等待的另一種定義是保證每個線程在其有限的步驟中正確計算自己的操作,而不管其他線程的操作,計時,交叉過速度,這一限制可以是系統中線程數的函數: 例如 , 如果有10個線程,每個線程都執行一次 CasCounter.increment() 操作, 最壞的情況下每個線程將必須重試最多9次才能成功增加,)

  無阻塞算法被廣泛用於操作系統和JVM級別,進行諸如線程和進程調度等任務,雖然無鎖算法的實現比較復雜,但相對於基於鎖的備選算法,它們有許多優點:可以避免優先級倒置和死鎖等危險,競爭比較便宜,協調發生在更細的粒度級別,允許更高程度的並行機制等等 ... 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM