1. 樂觀鎖
樂觀鎖顧名思義就是在操作時很樂觀,認為操作不會產生併發問題(不會有其他執行緒對資料進行修改),因此不會上鎖。但是在更新時會判斷其他執行緒在這之前有沒有對資料進行修改,一般會使用版本號機制
或CAS(compare and swap)演算法
實現。
簡單理解:這裡的資料,別想太多,你儘管用,出問題了算我慫,即操作失敗後事務回滾、提示。
1.1 版本號機制
1.1.1 實現套路:
- 取出記錄時,獲取當前
version
- 更新時,帶上這個
version
- 執行更新時,
set version = newVersion where version = oldVersion
- 如果
version
不對,就更新失敗
核心SQL:
update table set name = `Aron`, version = version + 1 where id = #{id} and version = #{version};
1.1.2 例項-Mybatis-plus 樂觀鎖實現
原文檢視請點選 Mybatis-plus 樂觀鎖實現
1.2 CAS演算法
樂觀鎖的另一種技術技術,當多個執行緒嘗試使用CAS同時更新同一個變數時,只有其中一個執行緒能更新變數的值,而其它執行緒都失敗,失敗的執行緒並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。
CAS
操作中包含三個運算元 :
- 需要讀寫的記憶體位置
V
- 進行比較的預期原值
A
- 擬寫入的新值
B
如果記憶體位置V
的值與預期原值A
相匹配,那麼處理器會自動將該位置值更新為新值B
。否則處理器不做任何操作。無論哪種情況,它都會在 CAS
指令之前返回該位置的值(在 CAS
的一些特殊情況下將僅返回 CAS
是否成功,而不提取當前值)。CAS
有效地說明了“ 我認為位置 V
應該包含值 A
;如果包含該值,則將 B
放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。 ”這其實和樂觀鎖的衝突檢查+資料更新的原理是一樣的。
1.2.1 例項-concurrent包的實現
由於java
的CAS
同時具有 volatile
讀和volatile
寫的記憶體語義,因此Java
執行緒之間的通訊現在有了下面四種方式:
-
A
執行緒寫volatile
變數,隨後B
執行緒讀這個volatile
變數。 -
A
執行緒寫volatile
變數,隨後B
執行緒用CAS
更新這個volatile
變數。 -
A
執行緒用CAS
更新一個volatile
變數,隨後B
執行緒用CAS
更新這個volatile
變數。 -
A
執行緒用CAS
更新一個volatile
變數,隨後B
執行緒讀這個volatile
變數。
Java
的CAS
會使用現代處理器上提供的高效機器級別原子指令,這些原子指令以原子方式對記憶體執行讀-改-寫操作,這是在多處理器中實現同步的關鍵(從本質上來說,能夠支援原子性讀-改-寫指令的計算機器,是順序計算圖靈機的非同步等價機器,因此任何現代的多處理器都會去支援某種能對記憶體執行原子性讀-改-寫操作的原子指令)。同時,volatile
變數的讀/寫和CAS
可以實現執行緒之間的通訊。把這些特性整合在一起,就形成了整個concurrent
包得以實現的基石。
仔細分析concurrent
包的原始碼實現,會發現一個通用化的實現模式:
- 首先,宣告共享變數為
volatile
; - 然後,使用CAS的原子條件更新來實現執行緒之間的同步;
- 同時,配合以
volatile
的讀/寫和CAS
所具有的volatile
讀和寫的記憶體語義來實現執行緒
1.2.2 缺點
-
ABA
問題
比如說一個執行緒T1
從記憶體位置V
中取出A
,這時候另一個執行緒T2
也從記憶體中取出A
,並且T2
進行了一些操作變成了B
,然後T2
又將V
位置的資料變成A
,這時候執行緒T1
進行CAS
操作發現記憶體中仍然是A
,然後T1
操作成功。儘管執行緒T1
的CAS
操作成功,但可能存在潛藏的問題。
- 迴圈時間長開銷大
自旋CAS
(不成功,就一直迴圈執行,直到成功)如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM
能支援處理器提供的pause
指令那麼效率會有一定的提升,pause
指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline
),使CPU
不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出迴圈的時候因記憶體順序衝突(memory order violation
)而引起CPU流水線被清空(CPU pipeline flush
),從而提高CPU
的執行效率。
- 只能保證一個共享變數的原子操作
當對一個共享變數執行操作時,我們可以使用迴圈CAS的方式來保證原子操作,但是對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變數合併成一個共享變數來操作。比如有兩個共享變數i = 2,j = a
,合併一下ij = 2a
,然後用CAS
來操作ij。從Java 1.5
開始JDK提供了AtomicReference
類來保證引用物件之間的原子性,你可以把多個變數放在一個物件裡來進行CAS
操作。
2. 悲觀鎖
總是假設最壞的情況,每次取資料時都認為其他執行緒會修改,所以都會加(悲觀)鎖。一旦加鎖,不同執行緒同時執行時,只能有一個執行緒執行,其他的執行緒在入口處等待,直到鎖被釋放。
悲觀鎖在MySQL
、Java
有廣泛的使用
-
MySQL
的讀鎖、寫鎖、行鎖等 -
Java
的synchronized
關鍵字
3. 總結
讀的多,衝突機率小,樂觀鎖。
寫的多,衝突機率大,悲觀鎖。
覺得有用記得收藏、點贊哦!