面試必問:樂觀鎖與悲觀鎖

前言

小孩子才作選擇,我全都要,今天寫一下面試必問的內容:樂觀鎖與悲觀鎖。主要從如下幾方面來講:
程序員

  • 何爲樂觀鎖
  • 何爲悲觀鎖
  • 樂觀鎖經常使用實現方式
  • 悲觀鎖經常使用實現方式
  • 樂觀鎖的缺點
  • 悲觀鎖的缺點

寫文章的時候忽然收到朋友發來的消息,說烏茲退役了,LPL0006號選手斷開鏈接。願你鮮衣怒馬,一日看盡長安花,歷盡山河萬里,歸來還是曾經那個少年。來,跟我一塊兒喊一句:大道至簡-惟我自豪web

一、何爲樂觀鎖

樂觀鎖老是假設事情向着好的方向發展,就好比有些人天生樂觀,向陽而生!面試

樂觀鎖老是假設最好的狀況,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據。樂觀鎖適用於多讀的應用類型,由於樂觀鎖在讀取數據的時候不會去加鎖,這樣能夠省去了鎖的開銷,加大了系統的整個吞吐量。即時偶爾有衝突,這也無傷大雅,要麼從新嘗試提交要麼返回給用戶說跟新失敗,固然,前提是偶爾發生衝突,但若是常常產生衝突,上層應用會不斷的進行自旋重試,這樣反卻是下降了性能,得不償失。算法

二、何爲悲觀鎖

悲觀鎖老是假設事情向着壞的方向發展,就好比有些人經歷了某些事情,可能不太相信別人,只信任本身,身在黑暗,腳踩光明!數據庫

悲觀鎖每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞住,直到我釋放了鎖,別人才能拿到鎖,這樣的話,數據只有自己一個線程在修改,就確保了數據的準確性。所以,悲觀鎖適用於多寫的應用類型。編程

三、樂觀鎖經常使用實現方式

3.1 版本號機制

版本號機制就是在表中增長一個字段,version,在修改記錄的時候,先查詢出記錄,再每次修改的時候給這個字段值加1,判斷條件就是你剛纔查詢出來的值。看下面流程就明白了:
微信

  • 3.1.1 新增用戶信息表
CREATE TABLE `user_info` (
 `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',  `user_name` varchar(64) DEFAULT NULL COMMENT '用戶姓名',  `money` decimal(15,0) DEFAULT '0' COMMENT '剩餘金額(分)',  `version` bigint(20) DEFAULT '1' COMMENT '版本號',  PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB COMMENT='用戶信息表'; 複製代碼
  • 3.1.2 新增一條數據
INSERT INTO `user_info` (`user_name`, `money`, `version`) VALUES ('張三', 1000, 1);
複製代碼
  • 3.1.3 操做步驟
步驟 線程A 線程B
1 查詢張三數據,得到版本號爲1(SELECT * FROM user_info WHERE user_name = '張三';)
2 查詢張三數據,得到版本號爲1(SELECT * FROM user_info WHERE user_name = '張三';)
3 修改張三金額,增長100,版本號+1(UPDATE user_info SET money = money + 100, version = version + 1 WHERE user_name = '張三' AND version = 1;),返回修改條數爲1
4 修改張三金額,增長200,版本號+1(UPDATE user_info SET money = money + 200, version = version + 1 WHERE user_name = '張三' AND version = 1;),返回修改條數爲0
5 判斷修改條數爲是否爲0,是返回失敗,不然返回成功
6 判斷修改條數爲是否爲0,是返回失敗,不然返回成功
7 返回成功
8 返回失敗

3.2 CAS算法

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

  1. 須要讀寫的內存值 V(主內存中的變量值)
  2. 進行比較的值 A(克隆下來線程本地內存中的變量值)
  3. 擬寫入的新值 B(要更新的新值)

當且僅當 V 的值等於 A 時,CAS 經過原子方式用新值 B 來更新 V 的值,不然不會執行任何操做(比較和替換是一個 native 原子操做)。通常狀況下,這是一個自旋操做,即不斷的重試,看下面流程:
app

  • 3.2.1 CAS算法模擬數據庫更新數據(表仍是剛纔那個表,用戶張三的金額初始值爲1000),給用戶張三的金額增長100:
private void updateMoney(String userName){
 // 死循環  for (;;){  // 獲取張三的金額  BigDecimal money = this.userMapper.getMoneyByName(userName);  User user = new User();  user.setMoney(money);  user.setUserName(userName);  // 根據用戶名和金額進行更新(金額+100)  Integer updateCount = this.userMapper.updateMoneyByNameAndMoney(user);  if (updateCount != null && updateCount.equals(1)){  // 若是更新成功就跳出循環  break;  }  }  } 複製代碼
  • 3.2.2 流程圖以下:
步驟 線程A 線程B
1 從表中查詢出張三的money=1000,設置進行比較的值爲1000,要寫入的新值爲money + 100 = 1100(V:1000--A:1000--B:1100)
2 從表中查詢出張三的money=1000,設置進行比較的值爲1000,要寫入的新值爲money + 100 = 1100(V:1000--A:1000--B:1100)
3 更新張三的金額(UPDATE user_info SET money = money + 100 WHERE user_name = '張三' AND money = 1000;),返回更新條數爲1
4 更新張三的金額(UPDATE user_info SET money = money + 100 WHERE user_name = '張三' AND money = 1000;),返回更新條數爲0
5 跳出循環,返回更新成功
6 自旋再次從表中查詢出張三的money=1100,設置進行比較的值爲1100,要寫入的新值爲money + 100 = 1200(V:1100--A:1100--B:1200)
7 更新張三的金額(UPDATE user_info SET money = money + 100 WHERE user_name = '張三' AND money = 1100;),返回更新條數爲1
8 跳出循環,返回更新成功

看到這裏,明眼人都發現了一些CAS更新的小問題,至因而什麼問題呢、怎麼解決呢,放在下面來說,要否則下面幾條就沒得寫了。。。。。。編輯器

注意,這裏的加版本號機制和CAS出現ABA問題加版本號解決機制不是同一個。

四、悲觀鎖經常使用實現方式

4.1 ReentrantLock

可重入鎖就是悲觀鎖的一種,若是你看過前兩篇文章,對可重入鎖的原理就很清楚了,不清楚的話就看下以下的流程:

  • 假設同步狀態值爲0表示未加鎖,爲1加鎖成功
步驟 線程A 線程B
1 從主內存中克隆出同步狀態值爲0,設置進行比較的值爲0,要寫入的新值爲1(V:0--A:0--B:1)
2 從主內存中克隆出同步狀態值爲0,設置進行比較的值爲0,要寫入的新值爲1(V:0--A:0--B:1)
3 更新主內存,用A和主內存的值比較,0 = 0,加鎖成功,此時主內存值爲1
4 更新主內存,用A和主內存的值比較,0 != 1,加鎖失敗。
5 返回加鎖成功
6 執行業務邏輯 自旋再次嘗試更新主內存,用A和主內存的值比較,0 != 1,加鎖失敗
7 自旋再次嘗試更新主內存,用A和主內存的值比較,0 != 1,加鎖失敗
8 自旋再次嘗試更新主內存,用A和主內存的值比較,0 != 1,加鎖失敗
9 釋放鎖,設置同步狀態值爲0
10 自旋再次嘗試更新主內存,用A和主內存的值比較,0 = 0,加鎖成功,此時主內存值爲1

能夠看到,只要線程A獲取了鎖,還沒釋放的話,線程B是沒法獲取鎖的,除非A釋放了鎖,B才能獲取到鎖,加鎖的方式都是經過CAS去比較再交換,B會一直自旋去CAS,除非線程中斷或者獲取到了鎖,要否則就一直在自旋,這也就說明了爲啥悲觀鎖比起樂觀鎖來講更加消耗性能。

4.2 synchronized

其實和上面差很少的,只不過上面自身維護了一個volatile int類型的變量,用來描述獲取鎖與釋放鎖,而synchronized是靠指令判斷加鎖與釋放鎖的,以下代碼:

public class synchronizedTest {
  。。。。。。   public void synchronizedTest(){  synchronized (this){  mapper.updateMoneyByName("張三");  }  } } 複製代碼

上面代碼對應的流程圖以下:

步驟 線程A 線程B
1 調用synchronizedTest()方法
2 調用synchronizedTest()方法
3 插入monitorenter指令
4 執行業務邏輯 嘗試獲取monitorenter指令的全部權
5 執行業務邏輯 嘗試獲取monitorenter指令的全部權
6 執行業務邏輯 嘗試獲取monitorenter指令的全部權
7 業務邏輯執行完畢,插入monitorexit指令 嘗試獲取monitorenter指令的全部權,獲取成功,插入monitorenter指令
8 執行業務邏輯
9 執行業務邏輯
10 業務邏輯執行完畢,插入monitorexit指令

若是在某個線程執行synchronizedTest()方法的過程當中出現了異常,monitorexit指令會插入在異常處,ReentrantLock須要你手動去加鎖與釋放鎖,而synchronized是JVM來幫你加鎖和釋放鎖。

五、樂觀鎖的缺點

5.1.1 ABA 問題

上面在說樂觀鎖用CAS方式實現的時候有個問題,明眼人能發現的,不知道各位有沒有發現,問題以下:

步驟 線程A 線程B
1 從表中查詢出張三的money=1000,設置進行比較的值爲1000,要寫入的新值爲money + 100 = 1100(V:1000--A:1000--B:1100)
2 從表中查詢出張三的money=1000,設置進行比較的值爲1000,要寫入的新值爲money + 100 = 1100(V:1000--A:1000--B:1100)
3 更新張三的金額(UPDATE user_info SET money = money + 100 WHERE user_name = '張三' AND money = 1000;),返回更新條數爲1
4 更新張三的金額(UPDATE user_info SET money = money + 100 WHERE user_name = '張三' AND money = 1000;),返回更新條數爲0
5 跳出循環,返回更新成功
6 自旋再次從表中查詢出張三的money=1100,設置進行比較的值爲1100,要寫入的新值爲money + 100 = 1200(V:1100--A:1100--B:1200)
7 更新張三的金額(UPDATE user_info SET money = money + 100 WHERE user_name = '張三' AND money = 1100;),返回更新條數爲1(注意,問題在這裏,在步驟6,咱們查詢到money=1100,而咱們在這裏判斷的時候,能肯定money沒有被別的線程修改過嗎?答案是並不能,有可線程能C加了100,線程D減了100,而這裏的money值仍然是1100,這個問題被稱爲CAS操做的 "ABA"問題
8 跳出循環,返回更新成功
  • 解決方案:

給表增長一個version字段,每修改一次值加1,這樣就能在寫入的時候判斷獲取到的值有沒有被修改過,流程圖以下:

步驟 線程A 線程B
1 從表中查詢出張三的money=1000,version=1
2 從表中查詢出張三的money=1000,version=1
3 更新張三的金額(UPDATE user_info SET money = money + 100, version = version + 1 WHERE user_name = '張三' AND money = 1000 AND version = 1;),返回更新條數爲1
4 更新張三的金額(UPDATE user_info SET money = money + 100, version = version + 1 WHERE user_name = '張三' AND money = 1000 AND version = 1;),返回更新條數爲0
5 跳出循環,返回更新成功
6 自旋再次從表中查詢出張三的money=1100,version = 2
7 更新張三的金額(UPDATE user_info SET money = money + 100, version = version + 1 WHERE user_name = '張三' AND money = 1100 AND version = 2;),返回更新條數爲1
8 跳出循環,返回更新成功

5.1.2 循環時間長開銷大

自旋CAS(也就是不成功就一直循環執行直到成功)若是長時間不成功,會給CPU帶來很是大的執行開銷。我的想法是在死循環添加嘗試次數,達到嘗試次數還沒成功的話就返回失敗。不肯定有沒有什麼問題,歡迎指出。

5.1.3 只能保證一個共享變量的原子操做

CAS 只對單個共享變量有效,當操做涉及跨多個共享變量時 CAS 無效。可是從 JDK 1.5開始,提供了AtomicReference類來保證引用對象之間的原子性,你能夠把多個變量放在一個對象裏來進行 CAS 操做。因此咱們可使用鎖或者利用AtomicReference類把多個共享變量合併成一個共享變量來操做。

六、悲觀鎖的缺點

6.1 synchronized

  • 鎖的釋放狀況少,只在程序正常執行完成和拋出異常時釋放鎖;
  • 試圖得到鎖是不能設置超時;
  • 不能中斷一個正在試圖得到鎖的線程;
  • 沒法知道是否成功獲取到鎖;

6.2 ReentrantLock

  • 須要使用import 引入相關的Class;
  • 不能忘記在finally 模塊釋放鎖,這個看起來比synchronized 醜陋;
  • synchronized能夠放在方法的定義裏面, 而reentrantlock只能放在塊裏面. 比較起來, synchronized能夠減小嵌套;

結尾

若是你以爲個人文章對你有幫助話,歡迎關注個人微信公衆號:"一個快樂又痛苦的程序員"(無廣告,單純分享原創文章、已pj的實用工具、各類Java學習資源,期待與你共同進步)

相關文章
相關標籤/搜索