必問系列 | 12張圖帶你徹底理解Java中的各種鎖~

前文
大家好,我是小龍。
又和大家見面了~
今天想和大家聊聊關(guān)于Java中的各種鎖,有幸在秋招面試中經(jīng)常被問到這塊,所以詳細(xì)整理了一下和大家分享一下,下次面試遇到可以游刃有余~
再看正文之前,大家可以閉眼回想一下這樣些個(gè)問題。
面試官:“對(duì)鎖了解嗎?談?wù)勀阒赖逆i?何為樂觀鎖?怎樣實(shí)現(xiàn)?分段鎖哪里有體現(xiàn)?這樣設(shè)計(jì)有什么好處?”
如果你都能很快回憶起來(lái),說(shuō)明基礎(chǔ)還是很扎實(shí)的。不過(guò)也不要急,可以繼續(xù)往下看,說(shuō)不定就有意外收獲。
如果你感覺很模糊啦,那這篇文章正適合你,補(bǔ)錄進(jìn)行式,春招即將進(jìn)行式。秋招沒拿到理想offer的同學(xué)趕快收藏轉(zhuǎn)發(fā)起來(lái)。
本篇精華涉獵:

悲觀鎖與樂觀鎖
悲觀鎖
何為悲觀?永遠(yuǎn)處于一個(gè)消極悲觀狀態(tài),因此悲觀鎖覺得并發(fā)操作每次都可能有問題,于是每次都會(huì)加鎖。
稍微詳細(xì)解釋:
悲觀鎖認(rèn)為對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作,一定會(huì)發(fā)生修改的,哪怕沒有修改,也會(huì)認(rèn)為修改。因此對(duì)于同一份數(shù)據(jù)的并發(fā)操作,悲觀鎖采取加鎖的形式。悲觀的認(rèn)為,不加鎖并發(fā)操作一定會(huì)出問題。

如圖所示,假設(shè)現(xiàn)在有多個(gè)線程想操作同一個(gè)資源對(duì)象,可能有人就會(huì)想到使用互斥鎖進(jìn)行同步,而它的同步方式就是悲觀的。認(rèn)為如果不嚴(yán)格同步線程調(diào)用,便會(huì)出問題。因此,互斥鎖會(huì)將資源鎖定,只供一個(gè)線程使用。
樂觀鎖
何為樂觀?永遠(yuǎn)處于樂觀積極狀態(tài),因此樂觀鎖覺得并發(fā)操作期間是不會(huì)出問題的,操作數(shù)據(jù)不加鎖,只會(huì)最后更新數(shù)據(jù)時(shí)檢查數(shù)據(jù)有沒有被修改,沒有的話才更新。

通常樂觀鎖可以用CAS算法或則vision版本機(jī)制實(shí)現(xiàn)。在Java中,java.util.concurrent.atomic包下的原子類就是使用CAS實(shí)現(xiàn)的。
既然提到了CAS,這個(gè)也是面試高頻問題,這里便將 CAS 做個(gè)簡(jiǎn)單介紹,可以將樂觀鎖和 CAS 二者聯(lián)系起來(lái)理解。
CAS 是一種樂觀鎖實(shí)現(xiàn)機(jī)制,主要是三部分:內(nèi)存值+舊的預(yù)期值+要修改的值。每次修改數(shù)據(jù)先比較內(nèi)存中值與預(yù)期值是否相同,不同就自旋,相同才修改。實(shí)現(xiàn)依靠unsafe(里面全是native修飾的本地方法,可以直接調(diào)用操作系統(tǒng))+lock cmpxchg(底層依靠硬件指令)。
如圖,原本共享變量 old value=0 ,線程修改數(shù)據(jù)先比較內(nèi)存中的值是否為0,若為0,代表沒有線程占用,此時(shí)才修改為 new value=1,當(dāng)其他線程到達(dá),發(fā)現(xiàn)內(nèi)存值與 old value 不一樣了,便自旋等待。
CAS缺點(diǎn):
可能造成 ABA (version)問題——當(dāng)一個(gè)值從A被更新為B,然后又改回來(lái),普通 CAS 機(jī)制發(fā)現(xiàn)不了。一直 while 浪費(fèi)資源:若并發(fā)量高,許多線程反復(fù)嘗試更新變量又更新不成功,循環(huán)往復(fù),會(huì)給 CPU 帶來(lái)高消耗。 不能保證代碼塊原子性:只能保證一個(gè)變量的原子操作,代碼塊要用 sychronized。
樂觀鎖與悲觀鎖的使用場(chǎng)景
這兩種鎖各有自己優(yōu)缺點(diǎn),只有在合適的場(chǎng)景使用合適的鎖,才能將各自的優(yōu)勢(shì)最大化體現(xiàn)出來(lái)。
樂觀鎖適用于讀多寫少的場(chǎng)景,因?yàn)樗遣患渔i的,相較于悲觀鎖不用加鎖、釋放鎖,節(jié)省了開銷。但是若寫多,沖突嚴(yán)重,可能導(dǎo)致線程一直 while 自旋,浪費(fèi)資源,反而降低了性能。此時(shí)在這種寫多讀少的場(chǎng)景使用悲觀鎖就更合適。
獨(dú)占鎖和共享鎖
獨(dú)占鎖
獨(dú)享鎖也叫排他鎖,是指該鎖一次只能被一個(gè)線程所持有。如果線程T對(duì)數(shù)據(jù)A加上排它鎖后,則其他線程不能再對(duì)A加任何類型的鎖。獲得排它鎖的線程即能讀數(shù)據(jù)又能修改數(shù)據(jù)。JDK 中的 synchronized 和 JUC 中 Lock 的實(shí)現(xiàn)類就是互斥鎖。

共享鎖
共享鎖是指該鎖可被多個(gè)線程所持有。如果線程T對(duì)數(shù)據(jù)A加上共享鎖后,則其他線程只能對(duì)A再加共享鎖,不能加排它鎖。獲得共享鎖的線程只能讀數(shù)據(jù),不能修改數(shù)據(jù)。

需要注意的是,對(duì)于Java ReentrantLock 而言,其是獨(dú)享鎖。但是對(duì)于 Lock 的另一個(gè)實(shí)現(xiàn)類 ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨(dú)占鎖。
讀鎖的共享鎖可保證并發(fā)讀是非常高效的,讀寫,寫讀,寫寫的過(guò)程是互斥的。
獨(dú)占鎖與共享鎖也是通過(guò) AQS 來(lái)實(shí)現(xiàn)的,通過(guò)實(shí)現(xiàn)不同的方法,來(lái)實(shí)現(xiàn)獨(dú)享或者共享。
對(duì)于 synchronized 而言,當(dāng)然是獨(dú)占鎖。
公平鎖和非公平鎖
公平鎖
公平鎖 是指多個(gè)線程按照申請(qǐng)鎖的順序來(lái)獲取鎖,線程直接進(jìn)入隊(duì)列中排隊(duì),隊(duì)列中的第一個(gè)線程才能獲得鎖。

公平鎖的優(yōu)點(diǎn)是等待鎖的線程不會(huì)餓死。缺點(diǎn)是整體吞吐效率相對(duì)非公平鎖要低,等待隊(duì)列中除第一個(gè)線程以外的所有線程都會(huì)阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。
非公平鎖
非公平鎖 是多個(gè)線程加鎖時(shí)直接嘗試獲取鎖,獲取不到才會(huì)到等待隊(duì)列的隊(duì)尾等待。但如果此時(shí)鎖剛好可用,那么這個(gè)線程可以無(wú)需阻塞直接獲取到鎖,所以非公平鎖有可能出現(xiàn)后申請(qǐng)鎖的線程先獲取鎖的場(chǎng)景。
非公平鎖的優(yōu)點(diǎn)是可以減少喚起線程的開銷,整體的吞吐效率高,因?yàn)榫€程有幾率不阻塞直接獲得鎖,CPU不必喚醒所有線程。缺點(diǎn)是處于等待隊(duì)列中的線程可能會(huì)餓死,或者等很久才會(huì)獲得鎖。

在Java中 synchronized 是非公平鎖,ReentrantLock 可以是非公平鎖,也可以是公平鎖,默認(rèn)非公平鎖。
//此處創(chuàng)建一個(gè)非公平鎖,默認(rèn)就是非公平,true 表示公平,false 表示公平。
Lock?lock?=new?ReentrantLock(flase);
此處既然談到了 synchronized和 ReentrantLock,那么,便順便與Lock聯(lián)系起來(lái)做個(gè)簡(jiǎn)單的對(duì)比。
來(lái)源:synchronized 是關(guān)鍵字,lock是一個(gè)接口,實(shí)現(xiàn)類進(jìn)行鎖操作。 是否知道獲取鎖:synchronized 無(wú)法判斷鎖的狀態(tài),lock 可以判斷是否獲得鎖 (boolean b =lock.tryLock();) 是否釋放鎖:synchronized 自動(dòng)釋放鎖,lock 手動(dòng)釋放(容易死鎖) lock.unlock();synchronized 阻塞后,其他線程一直等待,lock有超時(shí)時(shí)間。 synchronized 可重入鎖(多次獲取同一把鎖),不可中斷,非公平鎖;lock,默認(rèn)為非公平鎖,可設(shè)置為公平。 調(diào)度機(jī)制:synchronized 使用 Object 對(duì)象本身的 wait 、 notify、notifyAll 調(diào)度機(jī)制,而 Lock 可以使用 Condition 進(jìn)行線程之間的調(diào)度。 是否響應(yīng)中斷:lock 等待鎖過(guò)程中可以用 interrupt 來(lái)中斷等待,而 synchronized 只能等待鎖的釋放,不能響應(yīng)中斷。
分段鎖
分段鎖其實(shí)是一種鎖的設(shè)計(jì),并不是具體的一種鎖,具體在 ConcurrentHashMap JDK1.7 版本有所體現(xiàn)。其并發(fā)的實(shí)現(xiàn)就是通過(guò)分段鎖的形式來(lái)實(shí)現(xiàn)高效的并發(fā)操作。

何為分段鎖?為何引入分段鎖?
不妨同 Hashtable 進(jìn)行比較,我們都知道 Hashtable 是并發(fā)安全的,但是它的效率卻很低下,因?yàn)樗鼘?span style="color: rgb(255, 76, 0);">整個(gè)表都鎖起來(lái)了。這樣就使得很大程度降低了性能。而 ConcurrentHashMap JDK1.7 引入了分段鎖。
一個(gè) Segment 就相當(dāng)于一把鎖,它只鎖住這個(gè)槽位,其他的并不受影響。ConcurrentHashMap 將 hash 表分為 16 個(gè)桶(默認(rèn)值),諸如get,put,remove 等常用操作只鎖當(dāng)前需要用到的桶。
試想,原來(lái) 只能一個(gè)線程進(jìn)入,現(xiàn)在卻能同時(shí)16個(gè)寫線程進(jìn)入(寫線程才需要鎖定,而讀線程幾乎不受限制,之后會(huì)提到),并發(fā)性的提升是顯而易見的。
ConcurrentHashMap JDK1.7 中 數(shù)據(jù)結(jié)構(gòu)是由 Segment 數(shù)組+ HashEntry 數(shù)組+鏈表組成的。
Segment 繼承了 ReentrantLock,一個(gè) Segment[i] 就是一把分段鎖。比起 Hashtable 鎖粒度更細(xì),性能更高。
一個(gè)Segment中包含一個(gè)HashEntry數(shù)組,每個(gè)HashEntry又是一個(gè)鏈表結(jié)構(gòu)
static?final?class?Segment<K,V>?extens?ReentrantLock?implements?Serializable{
transient?volatile?HashEntry[]?tables;
//.....
}
static?final?class?HashEntry<K,V>
{
??final?int?hash;
??final?K?key;
??volatile?V?value;
??volatile?HashEntry?next;
}
可重入鎖
可重入鎖 又名遞歸鎖,是指在同一個(gè)線程在外層方法獲取鎖的時(shí)候,在進(jìn)入內(nèi)層方法會(huì)自動(dòng)獲取鎖。
synchronized?void?getA()?throws?Exception{
??Thread.sleep(1000);
??getB();
}
synchronized?void?getB()?throws?Exception{
??Thread.sleep(1000);
}

對(duì)于Java ReetrantLock 而言,從名字就可以看出是一個(gè)重入鎖,其名字是Re entrant Lock 重新進(jìn)入鎖。對(duì)于 Synchronized 而言,也是一個(gè)可重入鎖。可重入鎖的一個(gè)好處是可一定程度避免死鎖。
此處是我分析 JDK 源碼畫的 AQS 源碼解析流程圖,可以帶你很好的理解關(guān)于非公平鎖和可重入鎖內(nèi)部具體實(shí)現(xiàn)。此處可能有點(diǎn)模糊,若有需要可以后臺(tái)回復(fù)【AQS】領(lǐng)取。

自旋鎖
自旋鎖 是線程在沒有獲取到鎖時(shí)不會(huì)立即阻塞,會(huì)一直while循環(huán)去嘗試獲取鎖,這個(gè)便稱為自旋。

自旋可以減少線程被掛起,從而減少線程上下文切換的消耗。
但是若鎖被一個(gè)線程長(zhǎng)時(shí)間占用,那么一直循環(huán)自旋,便會(huì)浪費(fèi)系統(tǒng)資源,反而降低了整體性能。
小龍有話說(shuō)
本文以圖解方式給大家介紹了Java中的各種鎖,這一塊面試也特喜歡考,希望大家可以下來(lái)再多看看這塊相關(guān)的知識(shí)~
如果喜歡這個(gè)圖解系列文章的朋友歡迎留言,關(guān)注,小龍致力于以最通俗易懂的方式給大家分享知識(shí)~
我是小龍,我們下期見。
你要是愿意,我就永遠(yuǎn)愛你
你要是不愿意,我就永遠(yuǎn)相思
——《愛你就像愛生命》

求一鍵三連:希望轉(zhuǎn)發(fā)、在看、分享給更多同學(xué)喲~
公眾號(hào):大廠進(jìn)階指南,專注分享后端技術(shù)、校招面試求職~
相逢必是緣分,希望大家給個(gè)小小的關(guān)注!您的支持是我莫大的動(dòng)力,更多優(yōu)質(zhì)好文等您來(lái)探索,愛你喲
粉絲福利:后臺(tái)回復(fù)【助力禮包】領(lǐng)取校招求職全套攻略;回復(fù)【基于人工智能的智慧校園助手】領(lǐng)取校招求職精品項(xiàng)目。
