小孩子才作選擇,我全都要
,今天寫一下面試必問的內容:樂觀鎖與悲觀鎖。主要從如下幾方面來講:
程序員
寫文章的時候忽然收到朋友發來的消息,說烏茲退役了,LPL0006號選手斷開鏈接。願你鮮衣怒馬,一日看盡長安花,歷盡山河萬里,歸來還是曾經那個少年。來,跟我一塊兒喊一句:大道至簡-惟我自豪
web
樂觀鎖老是假設事情向着好的方向發展,就好比有些人天生樂觀,向陽而生!面試
樂觀鎖老是假設最好的狀況,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據。樂觀鎖適用於多讀的應用類型,由於樂觀鎖在讀取數據的時候不會去加鎖,這樣能夠省去了鎖的開銷,加大了系統的整個吞吐量。即時偶爾有衝突,這也無傷大雅,要麼從新嘗試提交要麼返回給用戶說跟新失敗,固然,前提是偶爾發生衝突
,但若是常常產生衝突,上層應用會不斷的進行自旋重試,這樣反卻是下降了性能,得不償失。算法
悲觀鎖老是假設事情向着壞的方向發展,就好比有些人經歷了某些事情,可能不太相信別人,只信任本身,身在黑暗,腳踩光明!數據庫
悲觀鎖每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞住,直到我釋放了鎖,別人才能拿到鎖,這樣的話,數據只有自己一個線程在修改,就確保了數據的準確性。所以,悲觀鎖適用於多寫的應用類型。編程
版本號機制就是在表中增長一個字段,version
,在修改記錄的時候,先查詢出記錄,再每次修改的時候給這個字段值加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='用戶信息表'; 複製代碼
INSERT INTO `user_info` (`user_name`, `money`, `version`) VALUES ('張三', 1000, 1);
複製代碼
步驟 | 線程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 | 返回失敗 |
CAS即 compare and swap(比較與交換),是一種有名的無鎖算法。無鎖編程,即不使用鎖(沒有線程被阻塞)的狀況下實現多線程之間的變量同步,因此也叫非阻塞同步(Non-blocking Synchronization)。CAS 算法涉及到三個操做數:
多線程
當且僅當 V 的值等於 A 時,CAS 經過原子方式用新值 B 來更新 V 的值,不然不會執行任何操做(比較和替換是一個 native 原子操做)。通常狀況下,這是一個自旋操做,即不斷的重試,看下面流程:
app
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; } } } 複製代碼
步驟 | 線程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問題加版本號解決機制不是同一個。
可重入鎖就是悲觀鎖的一種,若是你看過前兩篇文章,對可重入鎖的原理就很清楚了,不清楚的話就看下以下的流程:
步驟 | 線程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,除非線程中斷或者獲取到了鎖,要否則就一直在自旋,這也就說明了爲啥悲觀鎖比起樂觀鎖來講更加消耗性能。
其實和上面差很少的,只不過上面自身維護了一個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來幫你加鎖和釋放鎖。
上面在說樂觀鎖用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 | 跳出循環,返回更新成功 |
自旋CAS(也就是不成功就一直循環執行直到成功)若是長時間不成功,會給CPU帶來很是大的執行開銷。我的想法是在死循環添加嘗試次數,達到嘗試次數還沒成功的話就返回失敗。不肯定有沒有什麼問題,歡迎指出。
CAS 只對單個共享變量有效,當操做涉及跨多個共享變量時 CAS 無效。可是從 JDK 1.5開始,提供了AtomicReference類來保證引用對象之間的原子性,你能夠把多個變量放在一個對象裏來進行 CAS 操做。因此咱們可使用鎖或者利用AtomicReference類把多個共享變量合併成一個共享變量來操做。
若是你以爲個人文章對你有幫助話,歡迎關注個人微信公衆號:"一個快樂又痛苦的程序員"(無廣告,單純分享原創文章、已pj的實用工具、各類Java學習資源,期待與你共同進步)