深入理解各種鎖

樂觀鎖、悲觀鎖

樂觀鎖對應於現實生活中樂觀的人,思考事情總往好的方向發展;悲觀鎖對應於現實生活悲觀的人,思考事情總往壞的方向發展。不同性格的人都有優缺點,不能拋開場景說一種人好而另一種人不好。

樂觀鎖和悲觀鎖是一種廣義上的概念,體現了看待線程同步問題的不同角度,在 iOS、Java、數據庫中都有此概念。

悲觀鎖

對於同一個數據的併發操作,悲觀鎖認爲自己在使用數據的時候一定會有別的線程來修改數據,因此在獲取數據的時候會先加鎖,確保數據不會被別的線程修改。
這種線程一旦得到鎖,其他需要鎖的線程就掛起。共享資源每次只給一個線程使用,其他線程阻塞,用完再把資源轉讓給其他線程。傳統的關係型數據庫就用到很多悲觀鎖這種幾隻,比如行鎖、表鎖、讀鎖、寫鎖等,都是在操作之前先上鎖。

樂觀鎖

樂觀鎖認爲自己在使用數據的時候不會有別的線程來修改數據,所以不會添加鎖,只是在更新數據的時候去判斷之前有沒有別的線程更新了這個數據,如果這個數據沒有被更新,當前線程將自己修改的數據成功寫入,如果數據已經被別的線程更新,則根據不同方式執行不同操作(例如報錯或者自動重試)。

可以根據版本號機制和 CAS 算法實現。

樂觀鎖適合多讀少寫的應用類型或者場景,即衝突真的很少發生的場景,這樣省去了鎖的開銷,加大了系統的吞吐量。但是如果多寫少讀的情況,一般會經常發生衝突,這樣會導致上層應用層不斷 retry,這樣反而降低了性能,所以一般建議多寫的場景下使用悲觀鎖比較合適。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-OuabkoXB-1585662237391)(https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-12-19-lock.png)]

樂觀鎖常見的實現方式

樂觀鎖一般使用版本號機制或者 CAS 算法實現。

1. 版本號機制

在數據表增加一個數據版本號 version 字段,表示數據被修改的次數,當數據被修改時, version 值加1。當線程1更新數據的時候,先拿到數據並讀取出 version 值,修改完數據進行提交更新的時候時,若讀取出的 version 值爲當前數據庫中 version 值相等時才更新,否則重試更新操作,直到更新成功。

舉個例子:
假設數據庫中賬戶信息表有一個字段 version,值爲1;當前賬戶餘額爲100。當需要對賬戶信息表進行更新的時候,需要讀取 version 字段,以及賬戶餘額信息

  • 用戶 A 讀出數據:version = 1,balance = 100。從賬戶餘額中扣除 50, balacne = 50

  • 用戶 B 比用戶 A 剛剛晚一點點時間,讀出數據 :version = 1, balance = 100。從賬戶餘額中扣除 20,balance = 80

  • 用戶 A 完成修改操作,需要提交更新,但是在更新之前會先判斷數據庫中的版本號 version 值和自己讀取到的 version 值是否一致,如果一致,則將版本號 version 字段的值加1(version = 2),連同賬戶扣除後的餘額(balance = 50),提交到數據庫服務器執行更新操作,此時由於提交數據中版本號大於數據庫記錄中的版本,則數據被更新,數據庫記錄 version = 2

  • 用戶 B 完成修改操作,同樣在更新之前先讀取數據庫中的版本號 version 值和自己讀取到的 version 值是否一致,但此時發現自己讀取到的 version = 1,數據庫中的 version = 2,很顯然不滿足“當前最後更新的版本號 version 與操作員第一次讀取到的版本號 version 相等”的樂觀鎖策略,因此用戶 B 的提交被駁回。

這樣,就避免了用戶 B 基於 version = 1 的舊數據修改的結果覆蓋用戶 A 操作的結果,

2. CAS 算法

compare and swap(比較與交換) ,是一種有名的無鎖算法。 無鎖編程,即在不實用鎖的情況下實現多線程之間的數據同步,也就是在沒有線程被阻塞的情況下實現變量的同步,所以也叫做非阻塞同步(Non-blocking Synchorization)。CAS 算法涉及到的三個操作數

  • 需要讀寫的內存值 V
  • 進行比較的值 A
  • 擬寫入的新值 B

當且僅當 V 的值等於 A 時,CAS 通過原子方式用新值 B 來更新 V,否則不會執行任何操作。比較和替換是一個原則操作。一般情況下是一個自旋操作,即不斷的重試

樂觀鎖的缺點

  1. ABA 問題
    如果一個變量 V 初次讀取的時候的值爲 A,並且在準備賦值的時候檢查到變量 V 的值仍然是 A,那麼可以說是 V 的值從來沒被其他線程修改嗎?很明顯不能,因爲有可能變量 V 的值,從 A 變到 B,然後又改回到 A,那麼 CAS 的標準就會認爲變量 V 從來沒被修改過,這類問題被成爲 CAS 的 ABA 問題。
  2. 循環時間長、開銷大
    自旋 CAS (也就是不成功就一直循環操作直到成功)如果長時間不成功,會給 CPU 帶來非常大的執行開銷。
  3. 只能保證一個共享變量的原則操作
    CAS 只對單個變量共享有效,當操作涉及到多個共享變量時,CSA 無效。

CAS 與 synchorized 的使用場景

一般來說, CAS 適用於樂觀鎖,多讀少寫場景,衝突一般較少,則自旋操作的情況非常少,不會消耗 CPU,該場景合適。synchorized 使用悲觀鎖,多寫少讀場景,衝突一般較多。

  1. 對於資源競爭比較少(線程衝突較輕)的情況,如果使用 synchorized 同步鎖進行線程阻塞和喚醒切換以及用戶內核態間的切換操作額外浪費 CPU 資源;而 CAS 基於硬件實現,不需要進入內核,不需要切換線程,操作自旋的機率較少,因此可以獲得更高的性能。
  2. 對於資源競爭嚴重(線程衝突嚴重)的情況,CAS 自旋的機率會比較大,從而浪費更多的 CPU 資源。效率低於 synchorized

參考資料

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