java中的樂觀鎖與悲觀鎖

一.樂觀鎖與悲觀鎖:

悲觀鎖:總是假設最壞的情況=>每次拿數據的時候都會上鎖,不同線程同時執行時,只能有一個線程執行,其他的線程在入口處等待,直到鎖被釋放.。
應用:傳統的數據庫,java 同步synchronized關鍵字。

樂觀鎖: 每次拿數據的時候都不會上鎖。不同線程同時執行時,可以同時進入執行,在最後更新數據的時候要檢查這些數據是否被其他線程修改了(version版本和執行初是否相同),沒有修改則進行更新,否則放棄本次操作。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。

二.樂觀鎖的一種實現方式-CAS(Compare and Swap 比較並交換):

鎖存在的問題:
java jdk1.5之前都是靠synchronized關鍵字保證同步的,這種通過使用一致的鎖定協議來協調對共享狀態的訪問,可以確保無論哪個線程持有共享變量的鎖,都採用獨佔的方式來訪問這些變量。這就是一種獨佔鎖,獨佔鎖其實就是一種悲觀鎖,所以可以說 synchronized 是悲觀鎖。

悲觀鎖機制存在以下問題:
1.在多線程競爭下,加鎖,釋放鎖會導致比較多的上下文切換和調度延時,引起性能問題。
2.一個線程持有鎖會導致其他需要此鎖的線程掛起。
3.如果一個優先級高的線程等待一個優先級低的線程釋放鎖會導致優先級倒置,引起性能風險。

相對悲觀鎖而言,樂觀鎖假設認爲數據一般情況下不會產生併發衝突,所以在數據進行提交更新的時候,纔會正式對數據是否產生併發衝突進行檢測,如果發現併發衝突了,則讓返回用戶錯誤的信息,讓用戶決定如何去做。

其實現方式有一種比較典型的就是 Compare and Swap ( CAS ) :衝突檢測和數據更新

CAS是樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。
CAS 操作中包含三個操作數 —— 需要讀寫的內存位置(V)、進行比較的預期原值(A)和擬寫入的新值(B)。如果內存位置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;  
        }  
    }  
     //compareAndSet 利用JNI(Java Native Interface)來完成CPU指令的操作
    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指令的操作。

四.CAS存在的問題

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

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

部分樂觀鎖的實現是通過版本號(version)的方式來解決ABA問題,樂觀鎖每次在執行數據的修改操作時,都會帶上一個版本號,一旦版本號和數據的版本號一致就可以執行修改操作並對版本號執行+1操作,否則就執行失敗。因爲每次操作的版本號都會隨之增加,所以不會出現ABA問題,因爲版本號只會增加不會減少。

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

總結

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章