樂觀鎖與悲觀鎖如何理解?

樂觀鎖

就是樂觀地認爲每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間有沒有人去更新這個數據,可以使用版本號等機制。

樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似於write_condition機制,其實都是提供的樂觀鎖,在Java中java.util.concurrent.atomic包下的原子變量類就是使用了一種樂觀鎖的實現方式CAS實現的。

樂觀鎖的實現方式

版本號機制

使用版本標識來確定讀到的數據與提交時的數據是否一致,提交後修改版本標識,不一致時可以採用丟棄和再次嘗試。

CAS

所謂的CAS即爲Compare and Swap,即當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其他線程都失敗,失敗的線程被告知本次競爭失敗並可以重新競爭或者掛起。

CAS(compare and swap)又叫做比較交換來鑑別線程是否出現衝突,出現衝突就重試當前操作直到沒有衝突爲止。

CAS操作中包含三個操作數—需要讀寫的內存位置V,進行比較的預期原值A,和擬寫入新的值B.如果內存位置V的值與預期的原值A相匹配,那麼處理器會自動將該位置值更新爲新值B,否則處理器不做任何操作。

CAS產生的問題

  • ABA問題:

因爲CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A就會變成1A - 2B-3A。

從Java1.5開始JDK的atomic包裏提供了一個類 AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。

  • 循環時間長開銷大

自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。
我們可以用等紅綠燈作爲例子。Java 線程的阻塞相當於熄火停車,而自旋狀態相當於怠速停車。如果紅燈的等待時間非常長,那麼熄火停車相對省油一些;如果紅燈的等待時間非常短,比如我們在同步代碼塊中只做了一個整型加法,那麼在短時間內鎖肯定會被釋放出來,因此怠速停車更合適。然而,對於JVM來說,它並不能看到紅燈的剩餘時間,也就沒法根據等待時間的長短來選擇是自旋還是阻塞。JVM給出的方案是自適應自旋,根據以往自旋等待時能否獲取鎖,來動態調整自旋的時間(循環數)。就我們的例子來說,如果之前不熄火等待了綠燈,那麼這次不熄火的時間就長一點;如果之前不熄火沒等待綠燈,那麼這次不熄火的時間就短一點。

  • 公平性

自旋狀態還帶來另外一個副作用,不公平的鎖機制。處於阻塞狀態的線程,無法立刻競爭被釋放的鎖。 然而,處於自旋狀態的線程,則很有可能優先獲得這把鎖

  • 只能保證一個共享變量的原子操作。

當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操作。比如有兩個共享變量i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java1.5開始JDK提供AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象裏來進行CAS操作。

悲觀鎖

總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。

傳統的關係型數據庫裏邊就用了這種鎖機制,比如行鎖、表鎖、讀鎖、寫鎖等,都是在操作之前先上鎖,再比如java裏面的synchronized關鍵字的實現夜視悲觀鎖

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