????????首先要先了解Java中的鎖酵使,這就不得不提synchronized關(guān)鍵字和concurrent 包中的ReentrantLock。
synchronized關(guān)鍵字
????????synchronized 可以用來修飾以下 3 個(gè)層面:
? ? ? ? ? ? ? ? 1焙糟、修飾實(shí)例方法:鎖對(duì)象是當(dāng)前實(shí)例對(duì)象口渔。不同實(shí)例對(duì)象是不互斥的。
? ? ? ? ? ? ? ? 2穿撮、修飾靜態(tài)類方法:鎖對(duì)象是當(dāng)前類的 Class 對(duì)象缺脉。因此即使在不同線程中調(diào)用不同實(shí)例對(duì)象痪欲,也會(huì)有互斥效果。
? ? ? ? ? ? ? ? 3攻礼、修飾代碼塊:synchronized 作用于代碼塊時(shí)业踢,鎖對(duì)象就是跟在后面括號(hào)中的對(duì)象。任何 Object 對(duì)象都可以當(dāng)作鎖對(duì)象秘蛔。
ReentrantLock
????????ReentrantLock 的使用同 synchronized 有點(diǎn)不同陨亡,它的加鎖和解鎖操作都需要手動(dòng)完成。
//lock為ReentrantLock的實(shí)例對(duì)象
lock.lock();
lock.unlock();
synchronized和ReentrantLock的區(qū)別
? ? ? ? 說到他們的區(qū)別深员,首先先要看看我們用了synchronized關(guān)鍵字的字節(jié)碼是什么樣子的负蠕。代碼如下:
? ? ? ? 使用javap -v xxx.class查看字節(jié)碼,如下:
????????可以看出在字節(jié)碼中加入了1個(gè)monitorenter和2個(gè)monitorexit倦畅。這是因?yàn)樘摂M機(jī)需要保證當(dāng)異常發(fā)生時(shí)也能釋放鎖遮糖。因此 2 個(gè) monitorexit 一個(gè)是代碼正常執(zhí)行結(jié)束后釋放鎖,一個(gè)是在代碼執(zhí)行異常時(shí)釋放鎖叠赐。
????????因此在使用synchronized關(guān)鍵字時(shí)欲账,虛擬機(jī)會(huì)自動(dòng)在同步代碼塊的開始和結(jié)束(或異常)位置添加 monitorenter 和 monitorexit 指令。
? ? ? ? 而在ReentrantLock的使用中芭概,應(yīng)該將 unlock 操作放在 finally 代碼塊中赛不。這是因?yàn)?ReentrantLock 與 synchronized 不同,當(dāng)異常發(fā)生時(shí) synchronized 會(huì)自動(dòng)釋放鎖罢洲,但是 ReentrantLock 并不會(huì)自動(dòng)釋放鎖踢故。因此好的方式是將 unlock 操作放在 finally 代碼塊中,保證任何時(shí)候鎖都能夠被正常釋放掉惹苗。?
????????ReentrantLock還可以實(shí)現(xiàn)公平鎖(ReentrantLock初始化時(shí)傳入Boolean值)殿较,讀寫鎖(ReentrantReadWriteLock)
公平鎖:通過同步隊(duì)列來實(shí)現(xiàn)多個(gè)線程按照申請(qǐng)鎖的順序獲取鎖。
讀寫鎖:寫操作開始到結(jié)束之間桩蓉,不能再有其他讀操作進(jìn)來淋纲,并且寫操作完成之后的更新數(shù)據(jù)需要對(duì)后續(xù)的讀操作可見。ReentrantReadWriteLock在讀操作時(shí)獲取讀鎖院究,寫操作時(shí)獲取寫鎖洽瞬。當(dāng)寫鎖被獲取到時(shí),后續(xù)的讀寫鎖都會(huì)被阻塞业汰,寫鎖釋放之后伙窃,所有操作繼續(xù)執(zhí)行。
synchronized實(shí)現(xiàn)原理——對(duì)象頭和 Monitor
1蔬胯、對(duì)象頭
????????Java 對(duì)象在內(nèi)存中的布局分為 3 部分:對(duì)象頭、實(shí)例數(shù)據(jù)位他、對(duì)齊填充氛濒。當(dāng)我們?cè)?Java 代碼中产场,使用 new 創(chuàng)建一個(gè)對(duì)象的時(shí)候,JVM 會(huì)在堆中創(chuàng)建一個(gè) instanceOopDesc 對(duì)象舞竿,這個(gè)對(duì)象中包含了對(duì)象頭以及實(shí)例數(shù)據(jù)京景。
????????其中 _mark 和 _metadata 一起組成了對(duì)象頭。_mark 是 markOop 類型數(shù)據(jù)骗奖,一般稱它為標(biāo)記字段(Mark Word)确徙,其中主要存儲(chǔ)了對(duì)象的 hashCode、分代年齡执桌、鎖標(biāo)志位鄙皇,是否偏向鎖等。用一張圖來表示 32 位 Java 虛擬機(jī)的 Mark Word 的存儲(chǔ)結(jié)構(gòu)如下:
從圖中可以看出仰挣,根據(jù)"鎖標(biāo)志位”以及"是否為偏向鎖"伴逸,Java 中的鎖可以分為以下幾種狀態(tài)
????????在 Java 6 之前,并沒有輕量級(jí)鎖和偏向鎖膘壶,只有重量級(jí)鎖错蝴,也就是通常所說 synchronized 的對(duì)象鎖,鎖標(biāo)志位為 10颓芭。從圖3中的描述可以看出:當(dāng)鎖是重量級(jí)鎖時(shí)顷锰,對(duì)象頭中 Mark Word 會(huì)用 30 bit 來指向一個(gè)“互斥量”,而這個(gè)互斥量就是 Monitor。
Monitor
????????Monitor 可以把它理解為一個(gè)同步工具,也可以描述為一種同步機(jī)制荠雕。實(shí)際上适揉,它是一個(gè)保存在對(duì)象頭中的一個(gè)對(duì)象。markOop代碼中唯竹, monitor() 方法創(chuàng)建一個(gè) ObjectMonitor 對(duì)象,而 ObjectMonitor 就是 Java 虛擬機(jī)中的 Monitor 的具體實(shí)現(xiàn)。因此 Java 中每個(gè)對(duì)象都會(huì)有一個(gè)對(duì)應(yīng)的 ObjectMonitor 對(duì)象良狈,這也是 Java 中所有的 Object 都可以作為鎖對(duì)象的原因。
????????其中比較重要的屬性如下圖:
Java 虛擬機(jī)對(duì) synchronized 的優(yōu)化
????????從 Java 6 開始笨枯,虛擬機(jī)對(duì) synchronized 關(guān)鍵字做了多方面的優(yōu)化薪丁,主要目的就是,避免 ObjectMonitor 的訪問馅精,減少“重量級(jí)鎖”的使用次數(shù)严嗜,并最終減少線程上下文切換的頻率 。其中主要做了以下幾個(gè)優(yōu)化: 鎖自旋洲敢、輕量級(jí)鎖漫玄、偏向鎖。
鎖自旋
????????線程的阻塞和喚醒需要 CPU 從用戶態(tài)轉(zhuǎn)為核心態(tài),頻繁的阻塞和喚醒對(duì) CPU 來說是一件負(fù)擔(dān)很重的工作睦优,勢(shì)必會(huì)給系統(tǒng)的并發(fā)性能帶來很大的壓力渗常,所以 Java 引入了自旋鎖的操作。實(shí)際上自旋鎖在 Java 1.4 就被引入了汗盘,默認(rèn)關(guān)閉皱碘,但是可以使用參數(shù) -XX:+UseSpinning 將其開啟。但是從 Java 6 之后默認(rèn)開啟隐孽。
????????所謂自旋癌椿,就是讓該線程等待一段時(shí)間,不會(huì)被立即掛起菱阵,看當(dāng)前持有鎖的線程是否會(huì)很快釋放鎖踢俄。而所謂的等待就是執(zhí)行一段無意義的循環(huán)即可(自旋)。
????????自旋鎖也存在一定的缺陷:自旋鎖要占用 CPU送粱,如果鎖競(jìng)爭(zhēng)的時(shí)間比較長(zhǎng)褪贵,那么自旋通常不能獲得鎖,白白浪費(fèi)了自旋占用的 CPU 時(shí)間抗俄。這通常發(fā)生在鎖持有時(shí)間長(zhǎng)脆丁,且競(jìng)爭(zhēng)激烈的場(chǎng)景中,此時(shí)應(yīng)主動(dòng)禁用自旋鎖动雹。
輕量級(jí)鎖
????????有時(shí)候 Java 虛擬機(jī)中會(huì)存在這種情形:對(duì)于一塊同步代碼槽卫,雖然有多個(gè)不同線程會(huì)去執(zhí)行,但是這些線程是在不同的時(shí)間段交替請(qǐng)求這把鎖對(duì)象胰蝠,也就是不存在鎖競(jìng)爭(zhēng)的情況歼培。在這種情況下,鎖會(huì)保持在輕量級(jí)鎖的狀態(tài)茸塞,從而避免重量級(jí)鎖的阻塞和喚醒操作躲庄。
????????當(dāng)線程執(zhí)行某同步代碼時(shí),Java 虛擬機(jī)會(huì)在當(dāng)前線程的棧幀中開辟一塊空間(Lock Record)作為該鎖的記錄钾虐,然后 Java 虛擬機(jī)會(huì)嘗試使用 CAS(Compare And Swap)操作噪窘,將鎖對(duì)象的 Mark Word 拷貝到這塊空間中,并且將鎖記錄中的 owner 指向 Mark Word效扫。
????????當(dāng)線程再次執(zhí)行此同步代碼塊時(shí)倔监,判斷當(dāng)前對(duì)象的 Mark Word 是否指向當(dāng)前線程的棧幀,如果是則表示當(dāng)前線程已經(jīng)持有當(dāng)前對(duì)象的鎖菌仁,則直接執(zhí)行同步代碼塊浩习;否則只能說明該鎖對(duì)象已經(jīng)被其他線程搶占了,這時(shí)輕量級(jí)鎖需要膨脹為重量級(jí)鎖济丘。
????????輕量級(jí)鎖所適應(yīng)的場(chǎng)景是線程交替執(zhí)行同步塊的場(chǎng)合谱秽,如果存在同一時(shí)間訪問同一鎖的場(chǎng)合,就會(huì)導(dǎo)致輕量級(jí)鎖膨脹為重量級(jí)鎖。
偏向鎖
????????輕量級(jí)鎖是在沒有鎖競(jìng)爭(zhēng)情況下的鎖狀態(tài)疟赊,但是在有些時(shí)候鎖不僅存在多線程的競(jìng)爭(zhēng)辱士,而且總是由同一個(gè)線程獲得。因此為了讓線程獲得鎖的代價(jià)更低引入了偏向鎖的概念听绳。偏向鎖的意思是如果一個(gè)線程獲得了一個(gè)偏向鎖,如果在接下來的一段時(shí)間中沒有其他線程來競(jìng)爭(zhēng)鎖异赫,那么持有偏向鎖的線程再次進(jìn)入或者退出同一個(gè)同步代碼塊椅挣,不需要再次進(jìn)行搶占鎖和釋放鎖的操作。偏向鎖可以通過 -XX:+UseBiasedLocking 開啟或者關(guān)閉塔拳。
????????偏向鎖的具體實(shí)現(xiàn)就是在鎖對(duì)象的對(duì)象頭中有個(gè) ThreadId 字段鼠证,默認(rèn)情況下這個(gè)字段是空的,當(dāng)?shù)谝淮潍@取鎖的時(shí)候靠抑,就將自身的 ThreadId 寫入鎖對(duì)象的 Mark Word 中的 ThreadId 字段內(nèi)量九,將是否偏向鎖的狀態(tài)置為 01。這樣下次獲取鎖的時(shí)候颂碧,直接檢查 ThreadId 是否和自身線程 Id 一致荠列,如果一致,則認(rèn)為當(dāng)前線程已經(jīng)獲取了鎖载城,因此不需再次獲取鎖肌似,略過了輕量級(jí)鎖和重量級(jí)鎖的加鎖階段。提高了效率诉瓦。
????????其實(shí)偏向鎖并不適合所有應(yīng)用場(chǎng)景, 因?yàn)橐坏┏霈F(xiàn)鎖競(jìng)爭(zhēng)川队,偏向鎖會(huì)被撤銷,并膨脹成輕量級(jí)鎖睬澡,而撤銷操作(revoke)是比較重的行為固额,只有當(dāng)存在較多不會(huì)真正競(jìng)爭(zhēng)的 synchronized 塊時(shí),才能體現(xiàn)出明顯改善煞聪;因此實(shí)踐中斗躏,還是需要考慮具體業(yè)務(wù)場(chǎng)景,并測(cè)試后米绕,再?zèng)Q定是否開啟/關(guān)閉偏向鎖瑟捣。
對(duì)于鎖的幾種狀態(tài)轉(zhuǎn)換的源碼分析,可以參考:源碼分析Java虛擬機(jī)中鎖膨脹的過程
總結(jié)
????????首先說了Java 中兩個(gè)實(shí)現(xiàn)同步的方式 synchronized 和 ReentrantLock栅干。其中 synchronized使用更簡(jiǎn)單迈套,加鎖和釋放鎖都是由虛擬機(jī)自動(dòng)完成,而 ReentrantLock 需要開發(fā)者手動(dòng)去完成碱鳞。但是很顯然 ReentrantLock 的使用場(chǎng)景更多桑李,公平鎖還有讀寫鎖都可以在復(fù)雜場(chǎng)景中發(fā)揮重要作用。
????????另外,還介紹了Java 中鎖的幾種狀態(tài)贵白,其中偏向鎖和輕量級(jí)鎖都是通過自旋等技術(shù)避免真正的加鎖率拒,而重量級(jí)鎖才是獲取鎖和釋放鎖,重量級(jí)鎖通過對(duì)象內(nèi)部的監(jiān)視器(ObjectMonitor)實(shí)現(xiàn)禁荒,其本質(zhì)是依賴于底層操作系統(tǒng)的 Mutex Lock(互斥鎖) 實(shí)現(xiàn)猬膨,操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài)的切換,成本非常高呛伴。實(shí)際上Java對(duì)鎖的優(yōu)化還有”鎖消除“勃痴,但是”鎖消除“是基于Java對(duì)象逃逸分析的。