Synchronized 天天用,實現原理你懂嗎?

Java技術棧
www.javastack.cn
關注閱讀更多優(yōu)質文章
來源:小小木的博客
www.cnblogs.com/wyc1994666/p/11748212.html
Synchronized?關鍵字算是Java的元老級鎖了,一開始它撐起了Java的同步任務,其用法簡單粗暴容易上手。但是有些與它相關的知識點還是需要我們開發(fā)者去深入掌握的。
比如,我們都知道通過?Synchronized?鎖來實現互斥功能,可以用在方法或者代碼塊上,那么不同用法都是怎么實現的,以及都經歷了了哪些優(yōu)化等等問題都需要我們扎實的理解。
1.基本用法
2.實現原理
2.1 同步代碼塊的實現
2.2 同步方法的實現
3.鎖升級
3.1 Java對象頭介紹
3.2 什么是鎖升級
1.基本用法
通常我們可以把?Synchronized 用在一個方法或者代碼塊里,方法又有普通方法或者靜態(tài)方法。
對于普通同步方法,鎖是當前實例對象,也就是this
public?class?TestSyn{
??private?int?i=0;
??public?synchronized?void?incr(){
????i++;
??}
}
對于靜態(tài)同步方法,鎖是Class對象
public?class?TestSyn{
??private?static?int?i=0;
??public?static?synchronized?void?incr(){
????i++;
??}
}??
對于同步代碼塊,鎖是同步代碼塊里的對象
public?class?TestSyn{
??private??int?i=0;
??Object?o?=?new?Object();
??public??void?incr(){
????synchronized(o){
????????i++;
????}
??}
}
2.實現原理
在JVM規(guī)范中介紹了?Synchronized?的實現原理,JVM基于進入和退出Monitor對
象來實現方法同步和代碼塊同步,但兩者的實現細節(jié)不一樣。
代碼塊同步是使用monitorenter和monitorexit指令實現的,而方法同步是使用另外一種方式實現的,通過一個方法標志(flag) ACC_SYNCHRONIZED來實現的。
2.1 同步代碼塊的實現
monitorenter 和 monitorexit
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.monitorenter (參考來源)
下面看下JVM規(guī)范里對moniterenter 和 monitorexit的介紹
重點來了,上面這段介紹了兩點:
通過monitorenter和monitorexit指令來實現Java語言的同步代碼塊(后面有代碼示例)
monitorenter和monitorexit指令沒有被用在同步方法上?。?!

2.2 同步方法的實現
https://docs.oracle.com/javase/specs/jvms/se6/html/Compiling.doc.html#6530 (參考來源)
public?class?TestSyn?{
????private?int?i=0;
????
????//?同步方法
????public?synchronized?void?incer(){
????????i++;
????}
????
????//?同步代碼塊
????public??void?decr(){
????????synchronized?(this)?{
????????????i--;
????????}
????}
}
可以通過反編譯字節(jié)碼來查看底層是怎么實現的
//?得到字節(jié)碼javac?TestSyn.java
//?反編譯字節(jié)碼javap?-v?TestSyn.class
同步代碼塊的反編譯結果如下:

同步方法的反編譯結果如下:

3.鎖升級
3.1 Java對象頭介紹
對象的內存布局
在我們常見的HotSpot虛擬機中對象由三部分組成,分別是對象頭,實例數據,以及對齊填充位。
其中對象頭是跟鎖信息相關的部分,在對象頭里會存儲該對象運行時數據,包括哈希嗎,GC分代年齡,鎖狀態(tài)(無鎖,偏向鎖,輕量級鎖,重量級鎖),是否偏向鎖,偏向線程ID等信息。

對象頭的結構表示如下圖:

mark word的表示如下圖:

3.2 什么是鎖升級
下面舉個搶茅坑的例子來解釋一下鎖升級過程。
當只有一個線程訪問時叫做偏向鎖
假設我們每個廁所都有一把鑰匙,要想使用廁所首先必須得獲得鎖。某天上午員工甲急急忙忙的打完卡上廁所了,并在廁所門上貼了 “工號007使用中”的標簽,說明目前被工號007(相當于線程id)的員工占用呢,他再次向進入的時候只要上面的標簽還顯示工號007,他自己可以隨便進入,不需要再次上鎖了,有點偏向工號007員工的意思,所以這叫偏向鎖。
發(fā)生競爭的時候升級成輕量級鎖 (自旋等待)
員工甲正在使用廁所的時候,又來了兩個人想用廁所,但發(fā)現廁所被人使用著呢,無法獲得鎖。所以只能在外面等著甲出來,他們等的過程叫做“自旋”,這個叫做輕量級鎖。
那么又有一個問題,當甲出來之后正等著的那兩個人誰活得鎖呢?有兩種方式,按到達的順序來排隊或者不排隊,這兩種都可以實現,前者叫做公平鎖,后者叫做非公平鎖。
自旋等待沒結果的時候升級成重量級鎖
但那兩個人自旋一段時間之后發(fā)現甲還沒出來(JDK1.6規(guī)定為10次),一直這么等也不是個法子啊,所以打算向上升級,找?guī)芾韱T(操作系統(tǒng))反饋,升級成了重量級鎖了。
鎖的狀態(tài)總共有四種,無鎖狀態(tài)、偏向鎖、輕量級鎖和重量級鎖。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖。另外關注公眾號Java技術?;貜蚃VM46獲取一份46頁的JVM調優(yōu)教程。

鎖升級過程中mark word的變化如下:

偏向鎖
偏向鎖也是JDK 1.6中引入的一項鎖優(yōu)化, 引入它是為了優(yōu)化在沒有鎖競爭場景下的鎖消除。比如一段同步代碼一直是由單個線程調用,在這種場景下就沒必要使用同步鎖了,這里指的同步鎖不是指 synchronized,而是說沒不要到操作系統(tǒng)層面的互斥量了。
偏向鎖的偏向是指該同步代碼會一直偏向第一個調用它的線程,直到有別的線程過來競爭這把鎖,在第一次調用同步代碼并獲得鎖時會在對象頭和棧幀鎖記錄行(Lock Record)里存儲偏向線程Id,該線程在此進入的時候就不需要重新申請鎖了。只需檢測對象頭的Mark Word里是否存儲著指向該線程的ID即可。
直到又有線程來競爭這把鎖的時候偏向鎖會撤銷偏向。


輕量級鎖
輕量級鎖是JDK 1.6之中加入的新型鎖機制, 它名字中的“輕量級”是相對于使用操作系統(tǒng)。
互斥量來實現的傳統(tǒng)鎖而言的, 因此傳統(tǒng)的鎖機制就稱為“重量級”鎖。它并不是用來代替重量級鎖的, 它的本意是在統(tǒng)的重量級鎖使用操作系統(tǒng)互斥量產生的性能消耗。
線程在執(zhí)行同步塊之前,JVM會先在當前線程的棧楨中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復制到鎖記錄中,官方稱為Displaced Mark Word。
然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖.一直原地自旋,如果自旋數達到10次了則升級為重量級鎖。
重量級鎖
競爭的線程自旋一段時間未能獲取鎖之后會升級為重量級鎖,這個時候鎖的獲取與釋放都會由操作系統(tǒng)來分配了,如果持有鎖的線程釋放鎖之后操作系統(tǒng)會喚醒所有阻塞的哪些線程,并進入新一輪的爭搶模式,需要注意的是這些阻塞的線程沒有獲得鎖的優(yōu)先級,也就是說synchronized鎖是非公平的。
除此之外synchronized對中斷操作也是無感的,不會因為被中斷而放棄阻塞等待,它要么得到鎖要么一直阻塞。
點擊「閱讀原文」獲取面試題大全~
