開篇閑扯
打工人苍在,打工魂,我們生而人上人民泵。當(dāng)“資本主義”逐漸禁錮我們?nèi)耍ù螅┥希ň拢┤耍ú耍┤怏w的時(shí)候,那一刻我才明白那個(gè)日不落帝國(guó)·資本主義收割機(jī)·瑞民族之光幸·瑞幸咖啡是多么的了不起槽畔,盡管我不懂咖啡栈妆,但還是要說一聲謝謝!說到咖啡厢钧,喝完就想上廁所鳞尔,對(duì)寫bug的我來說太不友好了,畢竟我不(很)喜歡帶薪上廁所早直。
回歸本次的不正經(jīng)Java文章铅檩,本次新聞主要內(nèi)容有...tui~~嘴瓢了。上篇文章末尾處已經(jīng)提到了莽鸿,主要會(huì)把我對(duì)Synchronized的理解進(jìn)行一次全方位的梳理昧旨,如果能幫助到大家吊打面試官,萬分榮幸祥得。
Synchronized起源
那是個(gè)月黑風(fēng)高的夜晚兔沃,Doug Lee先生像我們一樣喝了咖啡憋著尿加班到深夜,只是他在寫JDK级及,我們?cè)谟盟腏DK寫B(tài)UG乒疏。在創(chuàng)作JDK1.5之前,他忘了在Java語言中提供同步可擴(kuò)展的同步接口或者方法了饮焦,于是在1.5之前給了我們一個(gè)惡Synchronized湊合用一下怕吴,而到了JDK1.5之后,增加了Lock接口及很多原生的并發(fā)包供我們使用县踢。因此转绷,Synchronized作為關(guān)鍵字的形式存在了很久,且在后續(xù)JDK1.6的版本中對(duì)它做了很多優(yōu)化硼啤,從而提升它的性能议经,使它能夠跟Lock有一戰(zhàn)之力。好了谴返,講完了煞肾,再見!
Synchronized是什么
如果我說嗓袱,Synchronized是一種基于JVM中對(duì)象監(jiān)視器的隱式非公平可重入重量級(jí)鎖(這頭銜跟瑞幸有一拼)籍救,加解鎖都是靠JVM內(nèi)部自動(dòng)實(shí)現(xiàn)的,吧啦吧啦...簡(jiǎn)稱"面試八股文"渠抹,很顯然我不能這么寫蝙昙,這樣還不如直接甩個(gè)博客鏈接來的快闪萄。來,解釋一下上面那句話耸黑,隱式鎖是基于操作系統(tǒng)的MutexLock實(shí)現(xiàn)的桃煎,每次加解鎖操作都會(huì)帶來用戶態(tài)與內(nèi)核態(tài)的切換篮幢,導(dǎo)致系統(tǒng)增加很多額外的開銷大刊。可以自行百度學(xué)習(xí)一下用戶態(tài)與內(nèi)核態(tài)的定義三椿,這里就不贅述了缺菌。同時(shí)Synchronized的加解鎖過程開發(fā)人員是不可控的,失去了可擴(kuò)展性搜锰。
接下來我們通過一個(gè)例子伴郁,看一看Synchronized在編譯后到底是什么樣子,上才(代)藝(碼):
/**
* FileName: SynchronizeDetail
* Author: RollerRunning
* Date: 2020/11/30 10:10 PM
* Description: 詳解Synchronized
*/
public class SynchronizeDetail {
public synchronized void testRoller() {
System.out.println("Roller Running!");
}
public void testRunning(){
synchronized (SynchronizeDetail.class){
System.out.println("Roller Running!");
}
}
}
將上面的源代碼進(jìn)行編譯再輸出編譯后的代碼:
public com.design.model.singleton.SynchronizeDetail();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 9: 0
public synchronized void testRoller();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Roller Running!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 14: 0
line 15: 8
public void testRunning();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #5 // class com/design/model/singleton/SynchronizeDetail
2: dup
3: astore_1
4: monitorenter
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #3 // String Roller Running!
10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
LineNumberTable:
line 17: 0
line 18: 5
line 19: 13
line 20: 23
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 18
locals = [ class com/design/model/singleton/SynchronizeDetail, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
觀察一下編譯后的代碼蛋叼,在testRoller()方法中有這樣一行描述flags: ACC_PUBLIC, ACC_SYNCHRONIZED焊傅,表示著當(dāng)前方法的訪問權(quán)限為SYNCHRONIZED的狀態(tài),而這個(gè)標(biāo)志就是編譯后由JVM根據(jù)Synchronized加鎖的位置增加的鎖標(biāo)識(shí)狈涮,也稱作類鎖狐胎,凡是要執(zhí)行該方法的線程,都需要先獲取Monitor對(duì)象歌馍,直到鎖被釋放以后才允許其他線程持有Monitor對(duì)象握巢。以HotSport虛擬機(jī)為例Monitor的底層又是基于C++ 實(shí)現(xiàn)的ObjectMonitor,我不懂C++松却,通過查資(百)料(度)查到了這個(gè)ObjectMonitor的結(jié)構(gòu)如下:
ObjectMonitor::ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; //線程重入次數(shù)
_object = NULL;
_owner = NULL; //標(biāo)識(shí)擁有該monitor的線程
_WaitSet = NULL; //由等待線程組成的雙向循環(huán)鏈表
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多線程競(jìng)爭(zhēng)鎖進(jìn)入時(shí)的單向鏈表
FreeNext = NULL ;
_EntryList = NULL ; //處于等待鎖block狀態(tài)的線程的隊(duì)列暴浦,也是一個(gè)雙向鏈表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
那么接下來就用一張圖說明一下多線程并發(fā)情況下獲取testRoller()方法鎖的過程
上文中提到了MutexLock,而圖中加解鎖獲取Monitor對(duì)象就是基于它實(shí)現(xiàn)的互斥操作晓锻,再次強(qiáng)調(diào)歌焦,在加解鎖過程中線程會(huì)存在內(nèi)核態(tài)與用戶態(tài)的切換,因此犧牲了一部分性能砚哆。
再來說一下testRunning()方法同规,很顯然,在編譯后的class中出現(xiàn)了一對(duì)monitorenter/monitorexit,其實(shí)就是對(duì)象監(jiān)視器的另一種形態(tài)窟社,本質(zhì)上是一樣的券勺,不過區(qū)別是,對(duì)象在鎖實(shí)例方法或者實(shí)例對(duì)象時(shí)稱作內(nèi)置鎖灿里。而上面的testRoller()是對(duì)類(對(duì)象的class)的權(quán)限控制关炼,兩者互不影響。
到這里就解釋Synchronized的基本概念匣吊,接下來要說一說它到底跟對(duì)象在對(duì)空間的內(nèi)存布局有什么關(guān)系儒拂。
Synchronized與對(duì)象堆空間布局
還是以64位操作系統(tǒng)下HotSport版本的JVM為例寸潦,看一張全網(wǎng)都搜的到的圖
圖中展示了MarkWord占用的64位在不同鎖狀態(tài)下記錄的信息,主要有對(duì)象的HashCode社痛、偏向鎖線程ID见转、GC年齡以及指向鎖的指針等,記住這里的GC標(biāo)志記錄的位置蒜哀,將來的JVM文章也會(huì)用到它斩箫,逃不掉的。在上篇例子中查看內(nèi)存布局的基礎(chǔ)上稍微改動(dòng)一下撵儿,代碼如下:
/**
* FileName: JavaObjectMode
* Author: RollerRunning
* Date: 2020/12/01 20:12 PM
* Description:查看加鎖對(duì)象在內(nèi)存中的布局
*/
public class JavaObjectMode {
public static void main(String[] args) {
//創(chuàng)建對(duì)象
Student student = new Student();
synchronized(student){
// 獲得加鎖后的對(duì)象布局內(nèi)容
String s = ClassLayout.parseInstance(student).toPrintable();
// 打印對(duì)象布局
System.out.println(s);
}
}
}
class Student{
private String name;
private String address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
第一張圖是上篇文章的也就是沒加鎖時(shí)對(duì)象的內(nèi)存布局乘客,第二張圖是加鎖后的內(nèi)存布局,觀察一下VALUE的值
其實(shí)加鎖后淀歇,就是修改了對(duì)象頭中MarkWord的值用來記錄當(dāng)前鎖狀態(tài)易核,所以可以看到加鎖前后VALUE發(fā)生了變化。
從第一張圖的第一行VALUE值可以看出當(dāng)前的鎖標(biāo)記為001(這里面涉及到一個(gè)大端序和小端序的問題浪默,可以自己學(xué)習(xí)一下:https://blog.csdn.net/limingliang_/article/details/80815393 )牡直,對(duì)應(yīng)的表中恰好是無鎖狀態(tài),實(shí)際代碼也是無鎖狀態(tài)纳决。而圖二可以看出當(dāng)前鎖標(biāo)記為000(提示:在上圖001同樣的位置)碰逸,對(duì)應(yīng)表中狀態(tài)為輕量級(jí)鎖,那么代碼中的的Synchronized怎么成了輕量級(jí)鎖了呢岳链?因?yàn)樵贘DK1.6以后對(duì)鎖進(jìn)行了優(yōu)化花竞,Synchronized會(huì)在競(jìng)爭(zhēng)逐漸激烈的過程中慢慢升級(jí)為重量級(jí)互斥鎖。
但是還有問題掸哑,為啥加鎖了约急,上來就是輕量級(jí)鎖而不是偏向鎖呢,原因是在初始化鎖標(biāo)記時(shí)JVM中默認(rèn)延遲4s創(chuàng)建偏向鎖苗分,由-XX:BiaseedLockingStartupDelay=xxx控制厌蔽。一旦創(chuàng)建偏向鎖,在沒有線程使用當(dāng)前偏向鎖時(shí)摔癣,叫做匿名偏向鎖奴饮,即上表中偏向線程ID的值為空,當(dāng)有一個(gè)線程過來加鎖時(shí)择浊,就進(jìn)化成了偏向鎖戴卜。
到這里,是不是已經(jīng)能看明白天天說的鎖也不過是一堆標(biāo)志位實(shí)現(xiàn)的琢岩,讓我寫幾個(gè)if-else就給你寫出來了
Synchronized鎖升級(jí)過程
鎖的升級(jí)過程為:偏向鎖-->偏向鎖-->輕量級(jí)鎖-->重量級(jí)鎖投剥。這個(gè)過程是隨著線程競(jìng)爭(zhēng)的激烈程度而逐漸變化的。
偏向鎖
其中匿名偏向鎖前面已經(jīng)說過了担孔,偏向鎖的作用就是當(dāng)同一線程多次訪問同步代碼時(shí)江锨,這一線程只需要獲取MarkWord中是否為偏向鎖吃警,再判斷偏向的線程ID是不是自己,就是倆if-else搞定啄育,Doug Lee先生不過如此嘛酌心。如果發(fā)現(xiàn)偏向的線程ID是自己的線程ID就去執(zhí)行代碼,不是就要通過CAS來嘗試獲取鎖挑豌,一旦CAS獲取失敗安券,就要執(zhí)行偏向鎖撤銷的操作。而這個(gè)過程在高并發(fā)的場(chǎng)景會(huì)代碼很大的性能開銷浮毯,慎重使用偏向鎖完疫。圖為偏向鎖的內(nèi)存布局
輕量級(jí)鎖
輕量級(jí)鎖是一種基于CAS操作的泰鸡,適用于競(jìng)爭(zhēng)不是很激烈的場(chǎng)景债蓝。輕量級(jí)鎖又分為自旋鎖和自適應(yīng)自旋鎖。自旋鎖:因?yàn)檩p量鎖是基于CAS理論實(shí)現(xiàn)的盛龄,因此當(dāng)資源被占用饰迹,其他線程搶鎖失敗時(shí),會(huì)被掛起進(jìn)入阻塞狀態(tài)余舶,當(dāng)資源就緒時(shí)啊鸭,再次被喚醒,這樣頻繁的阻塞喚醒申請(qǐng)資源匿值,十分低效赠制,因此產(chǎn)生了自旋鎖。JDK1.6中挟憔,JVM可以設(shè)置-XX:+UseSpinning參數(shù)來開啟自旋鎖钟些,使用-XX:PreBlockSpin來設(shè)置自旋鎖次數(shù)。不過到了JDK1.7及以后绊谭,取消自旋鎖參數(shù)政恍,JVM不再支持由用戶配置自旋鎖,因此出現(xiàn)了自適應(yīng)自旋鎖达传。自適應(yīng)自旋鎖:JVM會(huì)根據(jù)前一線程持有自旋鎖的時(shí)間以及鎖的擁有者的狀態(tài)進(jìn)行動(dòng)態(tài)決策獲取鎖失敗線程的自旋次數(shù)篙耗,進(jìn)而優(yōu)化因?yàn)檫^多線程自旋導(dǎo)致的大量CAS狀態(tài)的線程占用資源。下圖為輕量級(jí)鎖內(nèi)存布局:
隨著線程的增多宪赶,競(jìng)爭(zhēng)更加激烈以后宗弯,CAS等待已經(jīng)不能滿足需求,因此輕量級(jí)鎖又要向重量級(jí)鎖邁進(jìn)了搂妻。在JDK1.6之前升級(jí)的關(guān)鍵條件是超過了自旋等待的次數(shù)蒙保。在JDK1.7后,由于參數(shù)不可控叽讳,JVM會(huì)自行決定升級(jí)的時(shí)機(jī)追他,其中有幾個(gè)比較重要的因素:?jiǎn)蝹€(gè)線程持有鎖的時(shí)間坟募、線程在用戶態(tài)與內(nèi)核態(tài)之間切換的時(shí)間、掛起阻塞時(shí)間邑狸、喚醒時(shí)間懈糯、重新申請(qǐng)資源時(shí)間等
重量級(jí)鎖
而當(dāng)升級(jí)為重量級(jí)鎖的時(shí)候,就沒啥好說的了单雾,鎖標(biāo)記位為10赚哗,所有線程都要排隊(duì)順序執(zhí)行10標(biāo)記的代碼,前面提到的每一種鎖以及鎖升級(jí)的過程硅堆,其實(shí)都伴隨著MarkWord中鎖標(biāo)記位的變化屿储。相信看到這,大家應(yīng)該都理解了不同時(shí)期的鎖對(duì)應(yīng)著對(duì)象在堆空間中頭部不同的標(biāo)志信息渐逃。重量級(jí)鎖的內(nèi)存布局我模擬了半天也沒出效果够掠,有興趣的大佬可以講一下。
最后附上一張圖茄菊,展示一下鎖升級(jí)的過程疯潭,畫圖不易,還請(qǐng)觀眾老爺們關(guān)注懊嬷场:
鎖優(yōu)化
1.動(dòng)態(tài)編譯實(shí)現(xiàn)鎖消除
通過在編譯階段竖哩,使用編譯器對(duì)已加鎖代碼進(jìn)行逃逸性分析,判斷當(dāng)前同步代碼是否是只能被一個(gè)線程訪問脊僚,未被發(fā)布到其他線程(其他線程無權(quán)訪問)相叁。當(dāng)確認(rèn)后,會(huì)在編譯器辽幌,放棄生成Synchronized關(guān)鍵字對(duì)應(yīng)的字節(jié)碼增淹。
2.鎖粗化
在編譯階段,編譯器掃描到相鄰的兩個(gè)代碼塊都使用了Synchronized關(guān)鍵字舶衬,則會(huì)將兩者合二為一埠通,降低同一線程在進(jìn)出兩個(gè)同步代碼塊過程中帶來的性能損耗。
3.減小鎖粒度
這是開發(fā)層面需要做的事逛犹,即將鎖的范圍盡量明確并降低該范圍端辱,不能簡(jiǎn)單粗暴的加鎖。最佳實(shí)踐:在1.7及以前的ConcurrentHashMap中的分段鎖虽画。不過已經(jīng)不用了舞蔽。
最后,感謝各位觀眾老爺码撰,還請(qǐng)三連I痢!!
更多文章請(qǐng)微信搜索Java棧點(diǎn)公眾號(hào)朵栖!