Synchronized和lock區(qū)別
ReentrantLock可重入鎖的使用
一申眼、簡(jiǎn)述
synchronized 是一把經(jīng)典的 JVM 級(jí)別的鎖。在加了它的方法猾昆、代碼塊中态蒂,一次只允許一個(gè)線程進(jìn)入特定代碼段,從而避免多線程同時(shí)修改同一數(shù)據(jù)辽社。在 JDK6 之前伟墙,syncronized 是一把重量級(jí)的鎖,隨著 JDK 的升級(jí)滴铅,不斷的優(yōu)化戳葵,如今它變得不那么重了,甚至在某些場(chǎng)景下汉匙,它的性能反而優(yōu)于輕量級(jí)鎖拱烁。實(shí)現(xiàn)原理就是鎖升級(jí)的過(guò)程。
1??synchronized 的作用
- 原子性:保證語(yǔ)句塊內(nèi)操作是原子的噩翠。
- 可見(jiàn)性:保證可見(jiàn)性(通過(guò)“在執(zhí)行 unlock 之前戏自,必須先把此變量同步回主內(nèi)存”實(shí)現(xiàn))。
- 有序性:保證有序性(通過(guò)“一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行 lock 操作”)伤锚。
2??synchronized 的使用
- 修飾實(shí)例方法擅笔,對(duì)當(dāng)前實(shí)例對(duì)象加鎖。
- 修飾靜態(tài)方法屯援,多當(dāng)前類的 Class 對(duì)象加鎖猛们。
- 修飾代碼塊,對(duì) synchronized 括號(hào)內(nèi)的對(duì)象加鎖狞洋。
二弯淘、實(shí)現(xiàn)原理
JVM 的同步(synchronized)基于進(jìn)入和退出 Monitor 對(duì)象(即對(duì)象的監(jiān)視器,虛擬機(jī)規(guī)范中用的是管程一詞)實(shí)現(xiàn)吉懊,無(wú)論是顯式同步(有明確的 monitorenter 和 monitorexit 指令耳胎,即同步代碼塊)還是隱式同步都是如此惯吕。synchronized 是可重入的,所以不會(huì)自己把自己鎖死怕午。
1??代碼塊的同步顯示同步
利用 monitorenter 和 monitorexit 這兩個(gè)字節(jié)碼指令废登,配合完成了 synchronized 修飾代碼塊的互斥訪問(wèn)。
- 被 synchronized 修飾的代碼塊郁惜,編譯器編譯后:在代碼開(kāi)始加入了 monitorenter堡距,在代碼后面加入了 monitorexit。
- 在虛擬機(jī)執(zhí)行到 monitorenter 指令的時(shí)候兆蕉,會(huì)請(qǐng)求獲取對(duì)象的 monitor 鎖羽戒,基于 monitor 鎖又衍生出一個(gè)鎖計(jì)數(shù)器的概念。
- 當(dāng)執(zhí)行 monitorenter 時(shí)虎韵,若對(duì)象未被鎖定時(shí)易稠,或者當(dāng)前線程已經(jīng)擁有該對(duì)象的 monitor 鎖,則鎖計(jì)數(shù)器 +1包蓝,該線程獲取該對(duì)象鎖驶社。
- 當(dāng)執(zhí)行 monitorexit 時(shí),鎖計(jì)數(shù)器 -1测萎。當(dāng)計(jì)數(shù)器為 0 時(shí)亡电,此對(duì)象鎖就被釋放了。此時(shí)硅瞧,其它阻塞的線程可以請(qǐng)求獲取該 monitor 鎖份乒。
- 如果獲取 monitor 對(duì)象失敗,該線程則會(huì)進(jìn)入阻塞狀態(tài)腕唧,直到其他線程釋放鎖或辖。
2??方法級(jí)的同步隱式同步
方法級(jí)的同步是隱式,即無(wú)需通過(guò)字節(jié)碼指令來(lái)控制的枣接,它實(shí)現(xiàn)在方法調(diào)用和返回操作之中孝凌。JVM 可以從方法常量池中的方法表結(jié)構(gòu)(method_info Structure)中的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志區(qū)分一個(gè)方法是否同步方法。當(dāng)方法調(diào)用時(shí)月腋,調(diào)用指令將會(huì)檢查方法的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志,如果設(shè)置了瓣赂,執(zhí)行線程將先持有 monitor榆骚,然后再執(zhí)行方法,最后在方法(正常/非正常)完成時(shí)釋放 monitor煌集。
3??關(guān)于 monitorenter/monitorexit妓肢、ACC_SYNCHRONIZED 指令,可以看下下面的反編譯代碼:
public class SynchronizedDemo {
public void explicit() {
synchronized (this) {//這個(gè)是同步代碼塊
System.out.println("this method is explicit");
}
}
public synchronized void implicit() {//這個(gè)是同步方法
System.out.println("this method is implicit");
}
public static void main(String[] args) {
}
}
切換到目標(biāo)類目錄執(zhí)行javac SynchronizedDemo.java
命令生成編譯后的 .class 文件苫纤。執(zhí)行javap -c -s -v -l SynchronizedDemo
或javap -verbose SynchronizedDemo
反編譯后得到:
tips:通過(guò)javap SynchronizedDemo
可以查看其中的內(nèi)容纲缓。
三、JVM 對(duì) synchronized 的鎖優(yōu)化
Java6 為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗喊废,引入了“偏向鎖”和“輕量級(jí)鎖”祝高。鎖的狀態(tài)總共有四種,級(jí)別從低到高依次是:無(wú)鎖狀態(tài)污筷、偏向鎖工闺、輕量級(jí)鎖和重量級(jí)鎖。
隨著鎖的競(jìng)爭(zhēng)瓣蛀,鎖可以升級(jí)但不能降級(jí)陆蟆。
1??偏向鎖
偏向鎖是 Java6 之后加入的新鎖,它是一種針對(duì)加鎖操作的優(yōu)化手段惋增,目的是消除數(shù)據(jù)在無(wú)競(jìng)爭(zhēng)情況下的同步原語(yǔ)叠殷,進(jìn)一步提高程序的性能。通常诈皿,鎖不僅不存在多線程競(jìng)爭(zhēng)林束,而且還總是由同一線程多次獲得,為了減少同一線程獲取鎖(涉及到 CAS 操作纫塌,耗時(shí))的代價(jià)而引入偏向鎖诊县。偏向鎖的核心思想是,如果一個(gè)線程獲得了鎖措左,那么鎖就進(jìn)入偏向模式依痊,此時(shí) Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu),當(dāng)該線程再次請(qǐng)求鎖時(shí)怎披,無(wú)需再做任何同步操作胸嘁,即獲取鎖的過(guò)程,這樣就省去了大量有關(guān)鎖申請(qǐng)的操作凉逛,從而也就提升程序的性能性宏。所以,對(duì)于沒(méi)有鎖競(jìng)爭(zhēng)的場(chǎng)合状飞,偏向鎖有很好的優(yōu)化效果毫胜,畢竟極有可能連續(xù)多次是同一個(gè)線程申請(qǐng)相同的鎖。但是對(duì)于鎖競(jìng)爭(zhēng)比較激烈的場(chǎng)合诬辈,偏向鎖就失效了酵使,因?yàn)檫@樣場(chǎng)合每次申請(qǐng)鎖的線程極有可能都是不相同的,使用偏向鎖得不償失焙糟。注意口渔,偏向鎖失敗后,并不會(huì)立即膨脹為重量級(jí)鎖穿撮,而是先升級(jí)為輕量級(jí)鎖缺脉。
偏向鎖在 JDK8 中痪欲,默認(rèn)是輕量級(jí)鎖。但如果設(shè)定了-XX:BiasedLockingStartupDelay=0
攻礼,那在對(duì)一個(gè) Object 做 synchronized 的時(shí)候业踢,會(huì)立即上一把偏向鎖。當(dāng)處于偏向鎖狀態(tài)時(shí)秘蛔,markwork 會(huì)記錄當(dāng)前線程 ID陨亡。
- 判斷是否為可偏向狀態(tài)。
- 如果為可偏向狀態(tài)深员,則判斷線程 ID 是否是當(dāng)前線程负蠕,如果是進(jìn)入同步塊。
- 如果線程 ID 并未指向當(dāng)前線程倦畅,利用 CAS 操作競(jìng)爭(zhēng)鎖遮糖,如果競(jìng)爭(zhēng)成功,將 Mark Word 中線程 ID 更新為當(dāng)前線程 ID叠赐,進(jìn)入同步塊欲账。
- 如果競(jìng)爭(zhēng)失敗,等待全局安全點(diǎn)芭概,準(zhǔn)備撤銷偏向鎖赛不,根據(jù)線程是否處于活動(dòng)狀態(tài),決定是轉(zhuǎn)換為無(wú)鎖狀態(tài)還是升級(jí)為輕量級(jí)鎖罢洲。
當(dāng)鎖對(duì)象第一次被線程獲取的時(shí)候踢故,虛擬機(jī)會(huì)把對(duì)象頭中的標(biāo)志位設(shè)置為“01”,即偏向模式惹苗。同時(shí)使用 CAS 操作把獲取到這個(gè)鎖的線程 ID 記錄在對(duì)象的 Mark Word 中殿较。如果 CAS 操作成功,持有偏向鎖的線程以后每次進(jìn)入這個(gè)鎖相關(guān)的同步塊時(shí)桩蓉,虛擬機(jī)都可以不再進(jìn)行任何同步操作淋纲。
偏向鎖的釋放:
偏向鎖使用了遇到競(jìng)爭(zhēng)才釋放鎖的機(jī)制。偏向鎖的撤銷需要等待全局安全點(diǎn)院究,然后它會(huì)首先暫停擁有偏向鎖的線程洽瞬,然后判斷線程是否還活著,如果線程還活著业汰,則升級(jí)為輕量級(jí)鎖伙窃,否則,將鎖設(shè)置為無(wú)鎖狀態(tài)蔬胯。2??輕量級(jí)鎖
當(dāng)下一個(gè)線程參與到偏向鎖競(jìng)爭(zhēng)時(shí),會(huì)先判斷 markword 中保存的線程 ID 是否與這個(gè)線程 ID 相等位他,如果不相等氛濒,會(huì)立即撤銷偏向鎖产场,升級(jí)為輕量級(jí)鎖。每個(gè)線程在自己的線程棧中生成一個(gè) LockRecord(LR)舞竿,然后每個(gè)線程通過(guò) CAS(自旋) 的操作將鎖對(duì)象頭中的 markwork 設(shè)置為指向自己的 LR 的指針京景,哪個(gè)線程設(shè)置成功,就意味著獲得鎖骗奖。關(guān)于 synchronized 中此時(shí)執(zhí)行的 CAS 操作是通過(guò) native 的調(diào)用 HotSpot 中 bytecodeInterpreter.cpp 文件 C++ 代碼實(shí)現(xiàn)的确徙。
倘若偏向鎖失敗,虛擬機(jī)并不會(huì)立即升級(jí)為重量級(jí)鎖执桌,它還會(huì)嘗試使用一種稱為輕量級(jí)鎖的優(yōu)化手段(1.6 之后加入的)鄙皇,此時(shí) Mark Word 的結(jié)構(gòu)也變?yōu)檩p量級(jí)鎖的結(jié)構(gòu)。輕量級(jí)鎖能夠提升程序性能的依據(jù)是“對(duì)絕大部分的鎖仰挣,在整個(gè)同步周期內(nèi)都不存在競(jìng)爭(zhēng)”伴逸,注意這是經(jīng)驗(yàn)數(shù)據(jù)。需要了解的是膘壶,輕量級(jí)鎖所適應(yīng)的場(chǎng)景是線程交替執(zhí)行同步塊的場(chǎng)合错蝴,如果存在同一時(shí)間訪問(wèn)同一鎖的場(chǎng)合,就會(huì)導(dǎo)致輕量級(jí)鎖膨脹為重量級(jí)鎖颓芭。
3??自旋鎖
輕量級(jí)鎖失敗后顷锰,虛擬機(jī)為了避免線程真實(shí)地在操作系統(tǒng)層面掛起,還會(huì)進(jìn)行一項(xiàng)稱為自旋鎖的優(yōu)化手段亡问。這是基于在大多數(shù)情況下官紫,線程持有鎖的時(shí)間都不會(huì)太長(zhǎng),如果直接掛起操作系統(tǒng)層面的線程可能會(huì)得不償失玛界,畢竟操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)万矾,這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間,時(shí)間成本相對(duì)較高慎框,因此自旋鎖會(huì)假設(shè)在不久將來(lái)良狈,當(dāng)前的線程可以獲得鎖,因此虛擬機(jī)會(huì)讓當(dāng)前想要獲取鎖的線程做幾個(gè)空循環(huán)(這也是稱為自旋的原因)笨枯,一般不會(huì)太久薪丁,可能是 50 或 100 個(gè)循環(huán),經(jīng)過(guò)若干次循環(huán)后馅精,如果得到鎖就順利進(jìn)入臨界區(qū)严嗜。如果還不能獲得鎖,那就會(huì)將線程在操作系統(tǒng)層面掛起洲敢,這就是自旋鎖的優(yōu)化方式漫玄,這種方式確實(shí)也是可以提升效率的。最后沒(méi)辦法也就只能升級(jí)為重量級(jí)鎖了。
4??重量級(jí)鎖
如果鎖競(jìng)爭(zhēng)加劇(如線程自旋次數(shù)或者自旋的線程數(shù)超過(guò)某閾值睦优,JDK1.6 之后渗常,由 JVM 自己控制改規(guī)則),就會(huì)升級(jí)為重量級(jí)鎖汗盘。此時(shí)就會(huì)向操作系統(tǒng)申請(qǐng)資源皱碘,線程掛起,進(jìn)入到操作系統(tǒng)內(nèi)核態(tài)的等待隊(duì)列中隐孽,等待操作系統(tǒng)調(diào)度癌椿,然后映射回用戶態(tài)。在重量級(jí)鎖中菱阵,由于需要做內(nèi)核態(tài)到用戶態(tài)的轉(zhuǎn)換踢俄,而這個(gè)過(guò)程中需要消耗較多時(shí)間,也就是“重”的原因之一送粱。
Synchronized 的重量級(jí)鎖是通過(guò)對(duì)象內(nèi)部的一個(gè)叫做監(jiān)視器鎖(monitor)來(lái)實(shí)現(xiàn)的褪贵,監(jiān)視器鎖本質(zhì)又是依賴于底層的操作系統(tǒng)的 Mutex Lock(互斥鎖) 來(lái)實(shí)現(xiàn)的。而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)抗俄,這個(gè)成本非常高脆丁,狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間,這就是為什么 Synchronized 效率低的原因动雹。因此槽卫,這種依賴于操作系統(tǒng) Mutex Lock 所實(shí)現(xiàn)的鎖稱之為“重量級(jí)鎖”。
5??鎖消除
鎖消除是虛擬機(jī)另外一種鎖的優(yōu)化胰蝠,這種優(yōu)化更徹底歼培,Java 虛擬機(jī)在 JIT 編譯時(shí)(可以簡(jiǎn)單理解為當(dāng)某段代碼即將第一次被執(zhí)行時(shí)進(jìn)行編譯,又稱即時(shí)編譯)茸塞,通過(guò)對(duì)運(yùn)行上下文的掃描躲庄,去除不可能存在共享資源競(jìng)爭(zhēng)的鎖,通過(guò)這種方式消除沒(méi)有必要的鎖钾虐,可以節(jié)省毫無(wú)意義的請(qǐng)求鎖時(shí)間噪窘。如:
public void add() {
StringBuffer sb = new StringBuffer();
sb.append("a").append("b");
}
StringBuffer 的 append() 是一個(gè)同步方法。代碼中 add() 中的局部對(duì)象 sb效扫,只在該方法內(nèi)的作用域有效倔监,不可能被其他線程引用(因?yàn)槭蔷植孔兞浚瑮K接?菌仁。不同線程同時(shí)調(diào)用 add() 時(shí)浩习,都會(huì)創(chuàng)建不同的 sb 對(duì)象,sb 不可能存在共享資源競(jìng)爭(zhēng)的情景济丘。因此此時(shí)的 append() 若是同步谱秽,就是白白浪費(fèi)的系統(tǒng)資源。JVM 會(huì)自動(dòng)消除 StringBuffer 對(duì)象內(nèi)部的鎖。6??鎖粗化
如果虛擬機(jī)檢測(cè)到有這樣一串零碎的操作都對(duì)同一個(gè)對(duì)象加鎖疟赊,將會(huì)把加鎖同步的范圍擴(kuò)展(粗化)到整個(gè)操作序列的外部辱士。如:
public String test() {
int i = 0;
StringBuffer sb = new StringBuffer();
while (i < 100) {
sb.append("a");
i++;
}
return sb.toString();
}
JVM 會(huì)檢測(cè)到這樣一連串的操作都對(duì)同一個(gè)對(duì)象加鎖(while 循環(huán)內(nèi) 100 次執(zhí)行 append(),沒(méi)有鎖粗化就要進(jìn)行 100 次加鎖/解鎖)听绳,此時(shí) JVM 就會(huì)將加鎖的范圍粗化到這一連串的操作的外部(比如 while 循環(huán)體外),使得這一連串操作只需要加一次鎖即可异赫。