面試必備之樂觀鎖與悲觀鎖
悲觀鎖和樂觀鎖并不是某個具體的“鎖”而是一種并發(fā)編程的基本概念,是根據(jù)看待并發(fā)同步的角度;
悲觀鎖和樂觀鎖是用來解決并發(fā)問題的兩種思想,在不同的平臺有著各自的實現(xiàn)。
廢話不多,開始講解
1、悲觀鎖
- 悲觀鎖是基于一種悲觀的態(tài)度類來防止一切數(shù)據(jù)沖突,它是以一種預防的姿態(tài)在修改數(shù)據(jù)之前把數(shù)據(jù)鎖住,然后再對數(shù)據(jù)進行讀寫,在它釋放鎖之前任何人都不能對其數(shù)據(jù)進行操作,直到前面一個人把鎖釋放后下一個人數(shù)據(jù)加鎖才可對數(shù)據(jù)進行加鎖,然后才可以對數(shù)據(jù)進行操作,一般數(shù)據(jù)庫本身鎖的機制都是基于悲觀鎖的機制實現(xiàn)的;
- 特點:可以完全保證數(shù)據(jù)的獨占性和正確性,因為每次請求都會先對數(shù)據(jù)進行加鎖, 然后進行數(shù)據(jù)操作,最后再解鎖,而加鎖釋放鎖的過程會造成消耗,所以性能不高;
- 悲觀鎖通常多用于寫多比較多的情況下(多寫場景),避免頻繁失敗和重試影響性能
2、樂觀鎖
- 樂觀鎖是對于數(shù)據(jù)沖突保持一種樂觀態(tài)度,操作數(shù)據(jù)時不會對操作的數(shù)據(jù)進行加鎖(這使得多個任務可以并行的對數(shù)據(jù)進行操作),只有到數(shù)據(jù)提交的時候才通過一種機制來驗證數(shù)據(jù)是否存在沖突(一般實現(xiàn)方式是通過加版本號然后進行版本號的對比方式實現(xiàn));
- 特點:樂觀鎖是一種并發(fā)類型的鎖,其本身不對數(shù)據(jù)進行加鎖通而是通過業(yè)務實現(xiàn)鎖的功能,不對數(shù)據(jù)進行加鎖就意味著允許多個請求同時訪問數(shù)據(jù),同時也省掉了對數(shù)據(jù)加鎖和解鎖的過程,這種方式因為節(jié)省了悲觀鎖加鎖的操作,所以可以一定程度的的提高操作的性能,不過在并發(fā)非常高的情況下,會導致大量的請求沖突,沖突導致大部分操作無功而返而浪費資源,所以在高并發(fā)的場景下,樂觀鎖的性能卻反而不如悲觀鎖;
- 樂觀鎖通常多于寫比較少的情況下(多讀場景),避免頻繁加鎖影響性能,大大提升了系統(tǒng)的吞吐量;
3、實現(xiàn)樂觀鎖算法
樂觀鎖一般會使用版本號機制或 CAS 算法實現(xiàn)
①版本號機制
數(shù)據(jù)表中加上一個數(shù)據(jù)版本號 version 字段,表示數(shù)據(jù)被修改的次數(shù)。當數(shù)據(jù)被修改時,version 值會加一。當線程 A 要更新數(shù)據(jù)值時,在讀取數(shù)據(jù)的同時也會讀取 version 值,在提交更新時,若剛才讀取到的 version 值為當前數(shù)據(jù)庫中的 version 值相等時才更新,否則重試更新操作,直到更新成功
②CAS 算法
CAS 的思想很簡單,就是用一個預期值和要更新的變量值進行比較,兩值相等才會進行更新;
CAS 是一個原子操作,底層依賴于一條 CPU 的原子指令
CAS 涉及到三個操作數(shù):
- V :要更新的變量值(Var)
- E :預期值(Expected)
- N :擬寫入的新值(New)
當且僅當 V 的值等于 E 時,CAS 通過原子方式用新值 N 來更新 V 的值。如果不等,說明已經(jīng)有其它線程更新了V,則當前線程放棄更新;
4、樂觀鎖問題
①ABA 問題
- 一個變量 V 初次讀取的時候是 A 值,并且在準備賦值的時候檢查到它仍然是 A 值,那我們就能說明它的值沒有被其他線程修改過了嗎?很明顯是不能的,因為在這段時間它的值可能被改為其他值,然后又改回 A,那 CAS 操作就會誤認為它從來沒有被修改過。這個問題被稱為 CAS 操作的 "ABA"問題;
- ABA 問題的解決思路是在變量前面追加上版本號或者時間戳。JDK 1.5 以后的 AtomicStampedReference 類就是用來解決 ABA 問題的,其中的 compareAndSet() 方法就是首先檢查當前引用是否等于預期引用,并且當前標志是否等于預期標志,如果全部相等,則以原子方式將該引用和該標志的值設(shè)置為給定的更新值
②循環(huán)時間長開銷大
CAS 經(jīng)常會用到自旋操作來進行重試,也就是不成功就一直循環(huán)執(zhí)行直到成功。如果長時間不成功,會給 CPU 帶來非常大的執(zhí)行開
③只能保證一個共享變量的原子操作
CAS 只對單個共享變量有效,當操作涉及跨多個共享變量時 CAS 無效。但是從 JDK 1.5 開始,提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象里來進行 CAS 操作.所以我們可以使用鎖或者利用AtomicReference類把多個共享變量合并成一個共享變量來操作