1. 程式人生 > >深入Mysql鎖機制(五)樂觀鎖CAS

深入Mysql鎖機制(五)樂觀鎖CAS

深入Mysql鎖機制(五)樂觀鎖CAS

執行緒安全

眾所周知,Java是多執行緒的。但是,Java對多執行緒的支援其實是一把雙刃劍。一旦涉及到多個執行緒操作共享資源的情況時,處理不好就可能產生執行緒安全問題。執行緒安全性可能是非常複雜的,在沒有充足的同步的情況下,多個執行緒中的操作執行順序是不可預測的。

Java裡面進行多執行緒通訊的主要方式就是共享記憶體的方式,共享記憶體主要的關注點有兩個:可見性和有序性。加上覆合操作的原子性,我們可以認為Java的執行緒安全性問題主要關注點有3個:可見性、有序性和原子性。

Java記憶體模型(JMM)解決了可見性和有序性的問題,而鎖解決了原子性的問題。這裡不再詳細介紹JMM及鎖的其他相關知識。但是我們要討論一個問題,那就是鎖到底是不是有利無弊的?

鎖存在的問題

Java在JDK1.5之前都是靠synchronized關鍵字保證同步的,這種通過使用一致的鎖定協議來協調對共享狀態的訪問,可以確保無論哪個執行緒持有守護變數的鎖,都採用獨佔的方式來訪問這些變數。獨佔鎖其實就是一種悲觀鎖,所以可以說synchronized是悲觀鎖。

悲觀鎖機制存在以下問題:

在多執行緒競爭下,加鎖、釋放鎖會導致比較多的上下文切換和排程延時,引起效能問題。

一個執行緒持有鎖會導致其它所有需要此鎖的執行緒掛起。

如果一個優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖會導致優先順序倒置,引起效能風險。

而另一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試,直到成功為止。

與鎖相比,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操作成功,但是不代表這個過程就是沒有問題的。

那麼,樂觀鎖是如何解決ABA問題的呢?

部分樂觀鎖的實現是通過version版本號的方式來解決ABA問題,樂觀鎖每次在執行資料的修改操作時,都會帶上一個版本號,一旦版本號和資料的版本號一致就可以執行修改操作,否則就執行失敗。因為每次操作的版本號都會隨之增加,即使本次操作過程中資料被其他執行緒把資料從A改成B再改回A,那麼在寫操作進行的時候由於版本號與預期不一致也會執行失敗。