JUC鎖種類總結(jié)
點擊上方藍色字體,選擇“標星公眾號”
優(yōu)質(zhì)文章,第一時間送達
? 作者?|??萌新J
來源 |? urlify.cn/MfqEFn
文章正文:
在并發(fā)編程中有各種各樣的鎖,有的鎖對象一個就身兼多種鎖身份,所以初學者常常對這些鎖造成混淆,所以這里來總結(jié)一下這些鎖的特點和實現(xiàn)。
樂觀鎖、悲觀鎖
悲觀鎖
悲觀鎖是最常見的鎖,我們常說的加鎖指的也就是悲觀鎖。顧名思義,每次修改都抱著一種 "悲觀"?的態(tài)度,每次修改前都會認為有人會和他一樣執(zhí)行同一段代碼,所以每次修改時都會加鎖,而這個鎖就是悲觀鎖,加上悲觀鎖,其他線程不能執(zhí)行這段代碼,除非當前線程主動釋放鎖資源或者執(zhí)行完成正常釋放鎖資源。常見的悲觀鎖有 synchronized、ReentrantLock(常見對象時不加參數(shù)或者是false)。
適用場景:悲觀鎖適用于單個線程執(zhí)行時間長或者并發(fā)量高的場景,執(zhí)行時間長意味著上下文切換消耗的時間相當于線程執(zhí)行的時間就不算長,而并發(fā)量高則說明上下文切換的時間相當于多線程在隊列中等待消耗的時間也微不足道。
樂觀鎖
樂觀鎖,顧名思義,就是以一種樂觀的心態(tài)看待多線程對共享數(shù)據(jù)修改問題。認為當前線程在修改更新到主內(nèi)存中過程中(jmm,如果不熟悉可以移步多線程基礎(chǔ)總結(jié))沒有其他線程進行修改。所以在修改時并不會加鎖來限制其他線程,這樣的好處就是在其他線程沒有修改時效率更高,因為添加悲觀鎖就涉及到上下文切換,這樣在高并發(fā)場景就降低了程序的執(zhí)行效率。那么樂觀鎖又是如何實現(xiàn)的呢?就是通過三個值來實現(xiàn)的,分別是內(nèi)存地址存儲的實際值、預(yù)期值和更新值,當實際值與預(yù)期值相等時,就判定該值在修改過程中沒有發(fā)生改變,此時將值修改為更新值,而如果預(yù)期值和實際值不相等,則表示在更新期間有線程進行過修改,那么就修改失敗。CAS 就是樂觀鎖的一種實現(xiàn),因為用得比較多所以一般就用 CAS 來代指樂觀鎖,CAS 是 compareAndSet 方法名的縮寫,它是位于 JUC下 atomic 包下的類中的實現(xiàn)方法。在不同的類中實現(xiàn)方法不同,首先是普通的包裝類(AtomicInteger、AtomicBoolean等)和引用類(AtomicReference),他們的實現(xiàn)比較相似,都是兩個參數(shù)。下面就以 AtomicInteger 源碼為例,關(guān)于 atomic 相關(guān)介紹可以查看 atomic .
public?final?boolean?compareAndSet(int?expect,?int?update)?{
????????return?unsafe.compareAndSwapInt(this,?valueOffset,?expect,?update);
????}這里是調(diào)用 unsafe 對象的 compareAndSwapInt 方法,這里要知道 unSafe 類,unsafe 是 CAS 實現(xiàn)的核心類,在其內(nèi)部定義了很多 CAS 的實現(xiàn)方法。

? 可以看到內(nèi)部有許多 native 方法,native 方法是本地方法,其實現(xiàn)是使用 C、C++語言實現(xiàn)的,由于 C、C++語言與系統(tǒng)的兼容性更好,所以一些需要偏操作系統(tǒng)層面的操作還是使用 C與C++實現(xiàn)。而 unsafe 就是 CAS 實現(xiàn)的關(guān)鍵類。回到源碼,這里的 compareAndSet 方法是兩個參數(shù),預(yù)期值和更新值,但是內(nèi)部實現(xiàn)邏輯還是樂觀鎖的原理。
ABA 問題:上面這種實現(xiàn)是可能存在問題的,因為在比較時只會比較預(yù)期值和實際值,而可能這個值開始是A,這時開始進行 CAS 修改,所以預(yù)期值就是A,先修改進工作內(nèi)存,但是在這之間?實際值由 A 變成了 B,在 CAS 線程執(zhí)行 CAS 前又由 B 變回了 A,此時執(zhí)行 CAS 就會以為這個 A 是沒有變化的,所以正常更新為更新值,但其實它是改變過的。這就是樂觀鎖的 ABA 問題。這種問題在只需要比較開始和結(jié)束的業(yè)務(wù)中不需要管理,但是在需要數(shù)據(jù)一直保持不變的業(yè)務(wù)場景中就會變得很致命。
為了解決 ABA 問題,又提出版本號的概念,原理就是在修改前后比較的值從要修改的值變成版本號,每次數(shù)據(jù)變化時都會改變版本號,最終如果預(yù)期版本號和實際版本號相等就正常修改,如果不同就放棄修改。在 atomic包下這種思想的實現(xiàn)類就是 AtomicStampedReference,其 compareAndSet 源碼如下
public?boolean?compareAndSet(V???expectedReference,
?????????????????????????????????V???newReference,
?????????????????????????????????int?expectedStamp,
?????????????????????????????????int?newStamp)?{
????????Pair?current?=?pair;
????????return
????????????expectedReference?==?current.reference?&&
????????????expectedStamp?==?current.stamp?&&
????????????((newReference?==?current.reference?&&
??????????????newStamp?==?current.stamp)?||
?????????????casPair(current,?Pair.of(newReference,?newStamp)));
????}
四個參數(shù)分別為期望值,更新值,期望版本號,更新版本號。這里的實現(xiàn)是比較期望值和版本號兩個值。只有全部符合才會修改成功。
適用場景:樂觀鎖適用于執(zhí)行時間短且并發(fā)量小的場景,對于一段代碼只有幾個甚至只有一個線程同時執(zhí)行,那么樂觀鎖就可以起到大作用,因為它沒有加鎖解鎖的操作,線程不需要進行阻塞和喚醒,沒有上下文切換的時間損耗,同時如果并發(fā)量高的話樂觀鎖的效率也會遠低于悲觀鎖,因為樂觀鎖往往是與自旋鎖搭配使用的,而自旋鎖意味著會一直在執(zhí)行,一致占用 CPU,所以樂觀鎖只適用于執(zhí)行時間短且并發(fā)量小的場景。
自旋鎖
自旋鎖是不斷嘗試的鎖,一般與樂觀鎖搭配使用,因為悲觀鎖需要加鎖解鎖操作,這樣導致線程阻塞,經(jīng)過一次上下文切換后才能繼續(xù)執(zhí)行,這樣在并發(fā)量小且執(zhí)行時間短的場景中所消耗的時間就相對來說較長,所以在這種場景可以使用樂觀鎖+自旋鎖來實現(xiàn)。典型的這種實現(xiàn)就是 Atomic 包下的一些包裝類的方法實現(xiàn)。下面就以 AtomicInteger 的 getAndIncrement 方法源碼來解讀
public?final?int?getAndIncrement()?{
????????return?unsafe.getAndAddInt(this,?valueOffset,?1);
????}發(fā)現(xiàn)底層使用的還是 unsafe 類調(diào)用的方法。再點進這個方法
public?final?int?getAndAddInt(Object?var1,?long?var2,?int?var4)?{
????????int?var5;
????????do?{
????????????var5?=?this.getIntVolatile(var1,?var2);
????????}?while(!this.compareAndSwapInt(var1,?var2,?var5,?var5?+?var4));
????????return?var5;
????}
可以看到這里是使用了 while 循環(huán),在不斷嘗試調(diào)用 native 的 compareAndSwapInt 方法。這個 compareAndSwapInt 就是一個樂觀鎖方法,當執(zhí)行成功后就會跳出循環(huán),否則會一直嘗試執(zhí)行。
適用場景:因為一般是和樂觀鎖搭配使用的,所以和樂觀鎖適用場景一致。也就是線程執(zhí)行時間短且并發(fā)量低的場景。
共享鎖、獨占鎖
獨占鎖
獨占鎖又稱 “排它鎖”、"寫鎖"、“互斥鎖”,意為當前線程獲取到鎖之后其他線程就不能再獲取到鎖,我們常用的鎖如 synchronized、ReentrantLock 鎖都是獨占鎖。
共享鎖
共享鎖又稱 “讀鎖”,是指當前線程獲取到鎖之后其他線程也能獲取到當前鎖并執(zhí)行鎖中的代碼。可能有人會覺得如果這樣的話,那么共享鎖和無鎖有什么區(qū)別?事實上共享鎖一般是與獨占鎖搭配使用的,共享鎖與獨占鎖是互斥的,常用的共享鎖與獨占鎖實現(xiàn)類是 ReentrantReadWriteLock,關(guān)于這個鎖的使用可以查看 ReentrantReadWriteLock使用?。
適用場景:一般與獨占鎖搭配使用,用于某段代碼可以多個線程同時執(zhí)行但是與另外一段代碼互斥,比如A代碼同一時間可以有多個線程執(zhí)行,但是B代碼與A 代碼同一時間只能有一個線程執(zhí)行。
?
公平鎖、非公平鎖
非公平鎖
非公平鎖是指一段代碼塊被多個線程嘗試執(zhí)行,那么會有一個線程獲取到鎖資源,其他線程就會進入等待隊列等待,而新來的線程并不會直接進入阻塞隊列尾部,而是先嘗試獲取鎖資源,如果這時候之前占用鎖資源的線程剛好釋放鎖那么這個新來的線程就會直接獲取到鎖,而如果占用資源的線程沒有釋放鎖,那么新來的線程就獲取失敗,乖乖進入等待隊列尾部等待。synchronized 是非公平鎖,而 Lock 實現(xiàn)類的鎖會有兩種形式,公平鎖和非公平鎖,在創(chuàng)建對象時沒有指定參數(shù)默認就是非公平鎖。
適用場景:非公平鎖的優(yōu)勢是效率高,如果位于頭部的線程出現(xiàn)問題阻塞住了,也沒有獲取鎖資源,那么后面的線程就需要一直等待,直到其執(zhí)行完成,這樣就非常耗時,而非公平鎖則可以讓新來的線程直接獲取到鎖跳過其阻塞時間。缺點是這樣就會導致位于等待隊列尾部的線程獲取到鎖資源的機會很小,可能一直都沒有辦法獲取到鎖,造成 “饑餓”。適應(yīng)場景是要求執(zhí)行效率高,響應(yīng)時間短,同時每個線程的執(zhí)行時間較短的場景。
公平鎖
公平鎖是非公平鎖的對立面,也就是新來的線程直接進入阻塞隊列的尾部,而不會先嘗試獲取鎖資源。synchronized 只能是非公平鎖,而 Lock 系列的鎖在創(chuàng)建鎖對象時指定參數(shù)為 false 時就是公平鎖。
適用場景:公平鎖的優(yōu)勢是每個線程能按順序有序執(zhí)行,不會發(fā)生 “饑餓”?的情況,缺點是整個系統(tǒng)的執(zhí)行效率會低一些。適用于線程執(zhí)行時間較長,線程數(shù)較少,同時對線程執(zhí)行順序有要求的場景。
可重入鎖
可重入鎖指的是當前已經(jīng)獲取資源的線程在執(zhí)行內(nèi)部代碼時又遇到一個相同的鎖,比如下面這種場景。
public?synchronized?void?test(){
????????System.out.println("11");
????????synchronized?(this){
????????????System.out.println("2");
????????}
????}
這里方法上的 synchronized 對應(yīng)的對象和方法內(nèi)的 synchronized 對應(yīng)的對象是同一個對象,當某個對象進入方法后又遇到一個相同對象的鎖,那么如果這個鎖是可重入鎖,當前線程就會將當前鎖層次加1,相當于 AQS 機制中的 state,如果對 AQS 不熟悉可以看一下 AQS全解析?。每加一把鎖就會加1,釋放鎖就會減1。平時常用的鎖都是可重入鎖。
分段鎖
分段鎖是 ConcurrentHashMap 在1.7中的概念,因為在1.7中 ConcurrentHashMap 中使用的是分段鎖,也就是對數(shù)組的每一個元素進行加鎖,這樣就可以同時有16個線程(默認數(shù)組容量)一起操作。具體實現(xiàn)是在1.7中有一個內(nèi)部類 Segment,這個類內(nèi)部存儲的數(shù)據(jù)屬性就是 ConcurrentHashMap 數(shù)組某個下標對應(yīng)的鏈表所存儲的所有數(shù)據(jù),在需要同步的地方直接通過下標數(shù)獲取對應(yīng)的 Segment 對象然后進行加鎖,這樣將整個數(shù)組和其鏈表存儲的數(shù)據(jù)分段來加鎖就是分段鎖。關(guān)于 ConcurrentHashMap 和 HashMap 解析可以查看 HashMap 、ConcurrentHashMap知識點全解析?。
粉絲福利:實戰(zhàn)springboot+CAS單點登錄系統(tǒng)視頻教程免費領(lǐng)取
???
?長按上方微信二維碼?2 秒 即可獲取資料
感謝點贊支持下哈?
