樂觀鎖的一種實現方式——CAS

深刻理解樂觀鎖與悲觀鎖一文中咱們介紹過鎖。本文在這篇文章的基礎上,深刻分析一下樂觀鎖的實現機制,介紹什麼是CAS、CAS的應用以及CAS存在的問題等。java

線程安全

衆所周知,Java是多線程的。可是,Java對多線程的支持實際上是一把雙刃劍。一旦涉及到多個線程操做共享資源的狀況時,處理很差就可能產生線程安全問題。線程安全性多是很是複雜的,在沒有充足的同步的狀況下,多個線程中的操做執行順序是不可預測的。算法

Java裏面進行多線程通訊的主要方式就是共享內存的方式,共享內存主要的關注點有兩個:可見性和有序性。加上覆合操做的原子性,咱們能夠認爲Java的線程安全性問題主要關注點有3個:可見性、有序性和原子性。安全

Java內存模型(JMM)解決了可見性和有序性的問題,而鎖解決了原子性的問題。這裏再也不詳細介紹JMM及鎖的其餘相關知識。可是咱們要討論一個問題,那就是鎖究竟是不是有利無弊的?多線程

鎖存在的問題

Java在JDK1.5以前都是靠synchronized關鍵字保證同步的,這種經過使用一致的鎖定協議來協調對共享狀態的訪問,能夠確保不管哪一個線程持有共享變量的鎖,都採用獨佔的方式來訪問這些變量。獨佔鎖其實就是一種悲觀鎖,因此能夠說synchronized是悲觀鎖。併發

悲觀鎖機制存在如下問題:性能

在多線程競爭下,加鎖、釋放鎖會致使比較多的上下文切換和調度延時,引發性能問題。this

一個線程持有鎖會致使其它全部須要此鎖的線程掛起。spa

若是一個優先級高的線程等待一個優先級低的線程釋放鎖會致使優先級倒置,引發性能風險。線程

而另外一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止。指針

與鎖相比,volatile變量是一個更輕量級的同步機制,由於在使用這些變量時不會發生上下文切換和線程調度等操做,可是volatile不能解決原子性問題,所以當一個變量依賴舊值時就不能使用volatile變量。所以對於同步最終仍是要回到鎖機制上來。

樂觀鎖

樂觀鎖( Optimistic Locking)實際上是一種思想。相對悲觀鎖而言,樂觀鎖假設認爲數據通常狀況下不會形成衝突,因此在數據進行提交更新的時候,纔會正式對數據的衝突與否進行檢測,若是發現衝突了,則讓返回用戶錯誤的信息,讓用戶決定如何去作。

上面提到的樂觀鎖的概念中其實已經闡述了他的具體實現細節:主要就是兩個步驟:衝突檢測和數據更新。其實現方式有一種比較典型的就是Compare and Swap(CAS)。

CAS

CAS是項樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知此次競爭中失敗,並能夠再次嘗試。

CAS 操做包含三個操做數 —— 內存位置(V)、預期原值(A)和新值(B)。若是內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值。不然,處理器不作任何操做。不管哪一種狀況,它都會在 CAS 指令以前返回該位置的值。(在 CAS 的一些特殊狀況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明了「我認爲位置 V 應該包含值 A;若是包含該值,則將 B 放到這個位置;不然,不要更改該位置,只告訴我這個位置如今的值便可。」這其實和樂觀鎖的衝突檢查+數據更新的原理是同樣的。

這裏再強調一下,樂觀鎖是一種思想。CAS是這種思想的一種實現方式。

Java對CAS的支持

在JDK1.5 中新增java.util.concurrent(J.U.C)就是創建在CAS之上的。相對於對於synchronized這種阻塞算法,CAS是非阻塞算法的一種常見實現。因此J.U.C在性能上有了很大的提高。

咱們以java.util.concurrent中的AtomicInteger爲例,看一下在不使用鎖的狀況下是如何保證線程安全的。主要理解getAndIncrement方法,該方法的做用至關於 ++i 操做。

public class AtomicInteger extends Number implements java.io.Serializable {  

        private volatile int value;  

    public final int get() {  
        return value;  
    }  

    public final int getAndIncrement() {  
        for (;;) {  
            int current = get();  
            int next = current + 1;  
            if (compareAndSet(current, next))  
                return current;  
        }  
    }  

    public final boolean compareAndSet(int expect, int update) {  
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
    }  
}

在沒有鎖的機制下須要字段value要藉助volatile原語,保證線程間的數據是可見的。這樣在獲取變量的值的時候才能直接讀取。而後來看看++i是怎麼作到的。

getAndIncrement採用了CAS操做,每次從內存中讀取數據而後將此數據和+1後的結果進行CAS操做,若是成功就返回結果,不然重試直到成功爲止。而compareAndSet利用JNI來完成CPU指令的操做。

ABA問題

CAS會致使「ABA問題」。

CAS算法實現一個重要前提須要取出內存中某時刻的數據,而在下時刻比較並替換,那麼在這個時間差類會致使數據的變化。

好比說一個線程one從內存位置V中取出A,這時候另外一個線程two也從內存中取出A,而且two進行了一些操做變成了B,而後two又將V位置的數據變成A,這時候線程one進行CAS操做發現內存中仍然是A,而後one操做成功。儘管線程one的CAS操做成功,可是不表明這個過程就是沒有問題的。

部分樂觀鎖的實現是經過版本號(version)的方式來解決ABA問題,樂觀鎖每次在執行數據的修改操做時,都會帶上一個版本號,一旦版本號和數據的版本號一致就能夠執行修改操做並對版本號執行+1操做,不然就執行失敗。由於每次操做的版本號都會隨之增長,因此不會出現ABA問題,由於版本號只會增長不會減小。

總結

Java中的線程安全問題相當重要,要想保證線程安全,就須要鎖機制。鎖機制包含兩種:樂觀鎖與悲觀鎖。悲觀鎖是獨佔鎖,阻塞鎖。樂觀鎖是非獨佔鎖,非阻塞鎖。有一種樂觀鎖的實現方式就是CAS ,這種算法在JDK 1.5中引入的java.util.concurrent中有普遍應用。可是值得注意的是這種算法會存在ABA問題。

CAS與對象建立

另外,CAS還有一個應用,那就是在JVM建立對象的過程當中。對象建立在虛擬機中是很是頻繁的。即便是僅僅修改一個指針所指向的位置,在併發狀況下也不是線程安全的,可能正在給對象A分配內存空間,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的狀況。解決這個問題的方案有兩種,其中一種就是採用CAS配上失敗重試的方式保證更新操做的原子性。

相關文章
相關標籤/搜索