談?wù)?JVM 內(nèi)部鎖升級(jí)過(guò)程
一 為什么講這個(gè)?
總結(jié)AQS之后,對(duì)這方面順帶的復(fù)習(xí)一下。本文從以下幾個(gè)高頻問(wèn)題出發(fā):
對(duì)象在內(nèi)存中的內(nèi)存布局是什么樣的?
描述synchronized和ReentrantLock的底層實(shí)現(xiàn)和重入的底層原理。
談?wù)凙QS,為什么AQS底層是CAS+volatile?
描述下鎖的四種狀態(tài)和鎖升級(jí)過(guò)程?
Object o = new Object() 在內(nèi)存中占用多少字節(jié)?
自旋鎖是不是一定比重量級(jí)鎖效率高?
打開偏向鎖是否效率一定會(huì)提升?
重量級(jí)鎖到底重在哪里?
重量級(jí)鎖什么時(shí)候比輕量級(jí)鎖效率高,同樣反之呢?
二 加鎖發(fā)生了什么?
無(wú)意識(shí)中用到鎖的情況:
//System.out.println都加了鎖public void println(String x) { synchronized (this) { print(x); newLine(); }}
簡(jiǎn)單加鎖發(fā)生了什么?
要弄清楚加鎖之后到底發(fā)生了什么需要看一下對(duì)象創(chuàng)建之后再內(nèi)存中的布局是個(gè)什么樣的?
一個(gè)對(duì)象在new出來(lái)之后在內(nèi)存中主要分為4個(gè)部分:
markword這部分其實(shí)就是加鎖的核心,同時(shí)還包含的對(duì)象的一些生命信息,例如是否GC、經(jīng)過(guò)了幾次Young GC還存活。
klass pointer記錄了指向?qū)ο蟮腸lass文件指針。
instance data記錄了對(duì)象里面的變量數(shù)據(jù)。
padding作為對(duì)齊使用,對(duì)象在64位服務(wù)器版本中,規(guī)定對(duì)象內(nèi)存必須要能被8字節(jié)整除,如果不能整除,那么就靠對(duì)齊來(lái)補(bǔ)。舉個(gè)例子:new出了一個(gè)對(duì)象,內(nèi)存只占用18字節(jié),但是規(guī)定要能被8整除,所以padding=6。
知道了這4個(gè)部分之后,我們來(lái)驗(yàn)證一下底層。借助于第三方包 JOL = Java Object Layout java內(nèi)存布局去看看。很簡(jiǎn)單的幾行代碼就可以看到內(nèi)存布局的樣式:
- public class JOLDemo { private static Object o; public static void main(String[] args) { o = new Object(); synchronized (o){ System.out.println(ClassLayout.parseInstance(o).toPrintable()); } }}
將結(jié)果打印出來(lái):
從輸出結(jié)果看:
1)對(duì)象頭包含了12個(gè)字節(jié)分為3行,其中前2行其實(shí)就是markword,第三行就是klass指針。值得注意的是在加鎖前后輸出從001變成了000。Markword用處:8字節(jié)(64bit)的頭記錄一些信息,鎖就是修改了markword的內(nèi)容8字節(jié)(64bit)的頭記錄一些信息,鎖就是修改了markword的內(nèi)容字節(jié)(64bit)的頭記錄一些信息。從001無(wú)鎖狀態(tài),變成了00輕量級(jí)鎖狀態(tài)。
2)New出一個(gè)object對(duì)象,占用16個(gè)字節(jié)。對(duì)象頭占用12字節(jié),由于Object中沒(méi)有額外的變量,所以instance = 0,考慮要對(duì)象內(nèi)存大小要被8字節(jié)整除,那么padding=4,最后new Object() 內(nèi)存大小為16字節(jié)。
拓展:什么樣的對(duì)象會(huì)進(jìn)入老年代?很多場(chǎng)景例如對(duì)象太大了可以直接進(jìn)入,但是這里想探討的是為什么從Young GC的對(duì)象最多經(jīng)歷15次Young GC還存活就會(huì)進(jìn)入Old區(qū)(年齡是可以調(diào)的,默認(rèn)是15)。上圖中hotspots的markword的圖中,用了4個(gè)bit去表示分代年齡,那么能表示的最大范圍就是0-15。所以這也就是為什么設(shè)置新生代的年齡不能超過(guò)15,工作中可以通過(guò)-XX:MaxTenuringThreshold去調(diào)整,但是一般我們不會(huì)動(dòng)。
三 鎖的升級(jí)過(guò)程
1 鎖的升級(jí)驗(yàn)證
探討鎖的升級(jí)之前,先做個(gè)實(shí)驗(yàn)。兩份代碼,不同之處在于一個(gè)中途讓它睡了5秒,一個(gè)沒(méi)睡??纯词欠裼袇^(qū)別。
- public class JOLDemo { private static Object o; public static void main(String[] args) { o = new Object(); synchronized (o){ System.out.println(ClassLayout.parseInstance(o).toPrintable()); } }}----------------------------------------------------------------------------------------------public class JOLDemo { private static Object o; public static void main(String[] args) { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } o = new Object(); synchronized (o){ System.out.println(ClassLayout.parseInstance(o).toPrintable()); } }}
這兩份代碼會(huì)不會(huì)有什么區(qū)別?運(yùn)行之后看看結(jié)果:
有點(diǎn)意思的是,讓主線程睡了5s之后輸出的內(nèi)存布局跟沒(méi)睡的輸出結(jié)果居然不一樣。
Syn鎖升級(jí)之后,jdk1.8版本的一個(gè)底層默認(rèn)設(shè)置4s之后偏向鎖開啟。也就是說(shuō)在4s內(nèi)是沒(méi)有開啟偏向鎖的,加了鎖就直接升級(jí)為輕量級(jí)鎖了。
那么這里就有幾個(gè)問(wèn)題了?
為什么要進(jìn)行鎖升級(jí),以前不是默認(rèn)syn就是重量級(jí)鎖么?要么不用要么就用別的不行么?
既然4s內(nèi)如果加了鎖就直接到輕量級(jí),那么能不能不要偏向鎖,為什么要有偏向鎖?
為什么要設(shè)置4s之后開始偏向鎖?
問(wèn)題1:為什么要進(jìn)行鎖升級(jí)?鎖了就鎖了,不就要加鎖么?
首先明確早起jdk1.2效率非常低。那時(shí)候syn就是重量級(jí)鎖,申請(qǐng)鎖必須要經(jīng)過(guò)操作系統(tǒng)老大kernel進(jìn)行系統(tǒng)調(diào)用,入隊(duì)進(jìn)行排序操作,操作完之后再返回給用戶態(tài)。
內(nèi)核態(tài):用戶態(tài)如果要做一些比較危險(xiǎn)的操作直接訪問(wèn)硬件,很容易把硬件搞死(格式化,訪問(wèn)網(wǎng)卡,訪問(wèn)內(nèi)存干掉、)操作系統(tǒng)為了系統(tǒng)安全分成兩層,用戶態(tài)和內(nèi)核態(tài) 。申請(qǐng)鎖資源的時(shí)候用戶態(tài)要向操作系統(tǒng)老大內(nèi)核態(tài)申請(qǐng)。Jdk1.2的時(shí)候用戶需要跟內(nèi)核態(tài)申請(qǐng)鎖,然后內(nèi)核態(tài)還會(huì)給用戶態(tài)。這個(gè)過(guò)程是非常消耗時(shí)間的,導(dǎo)致早期效率特別低。有些jvm就可以處理的為什么還交給操作系統(tǒng)做去呢?能不能把jvm就可以完成的鎖操作拉取出來(lái)提升效率,所以也就有了鎖優(yōu)化。
問(wèn)題2:為什么要有偏向鎖?
其實(shí)這本質(zhì)上歸根于一個(gè)概率問(wèn)題,統(tǒng)計(jì)表示,在我們?nèi)粘S玫膕yn鎖過(guò)程中70%-80%的情況下,一般都只有一個(gè)線程去拿鎖,例如我們常使用的System.out.println、StringBuffer,雖然底層加了syn鎖,但是基本沒(méi)有多線程競(jìng)爭(zhēng)的情況。那么這種情況下,沒(méi)有必要升級(jí)到輕量級(jí)鎖級(jí)別了。偏向的意義在于:第一個(gè)線程拿到鎖,將自己的線程信息標(biāo)記在鎖上,下次進(jìn)來(lái)就不需要在拿去拿鎖驗(yàn)證了。如果超過(guò)1個(gè)線程去搶鎖,那么偏向鎖就會(huì)撤銷,升級(jí)為輕量級(jí)鎖,其實(shí)我認(rèn)為嚴(yán)格意義上來(lái)講偏向鎖并不算一把真正的鎖,因?yàn)橹挥幸粋€(gè)線程去訪問(wèn)共享資源的時(shí)候才會(huì)有偏向鎖這個(gè)情況。
無(wú)意使用到鎖的場(chǎng)景:
- /***StringBuffer內(nèi)部同步***/public synchronized int length() { return count;} //System.out.println 無(wú)意識(shí)的使用鎖public void println(String x) { synchronized (this) { print(x); newLine(); } }
問(wèn)題3:為什么jdk8要在4s后開啟偏向鎖?
其實(shí)這是一個(gè)妥協(xié),明確知道在剛開始執(zhí)行代碼時(shí),一定有好多線程來(lái)?yè)屾i,如果開了偏向鎖效率反而降低,所以上面程序在睡了5s之后偏向鎖才開放。為什么加偏向鎖效率會(huì)降低,因?yàn)橹型径嗔藥讉€(gè)額外的過(guò)程,上了偏向鎖之后多個(gè)線程爭(zhēng)搶共享資源的時(shí)候要進(jìn)行鎖升級(jí)到輕量級(jí)鎖,這個(gè)過(guò)程還的把偏向鎖進(jìn)行撤銷在進(jìn)行升級(jí),所以導(dǎo)致效率會(huì)降低。為什么是4s?這是一個(gè)統(tǒng)計(jì)的時(shí)間值。
當(dāng)然我們是可以禁止偏向鎖的,通過(guò)配置參數(shù)-XX:-UseBiasedLocking = false來(lái)禁用偏向鎖。jdk15之后默認(rèn)已經(jīng)禁用了偏向鎖。本文是在jdk8的環(huán)境下做的鎖升級(jí)驗(yàn)證。
2 鎖的升級(jí)流程
上面已經(jīng)驗(yàn)證了對(duì)象從創(chuàng)建出來(lái)之后進(jìn)內(nèi)存從無(wú)鎖狀態(tài)->偏向鎖(如果開啟了)->輕量級(jí)鎖的過(guò)程。對(duì)于鎖升級(jí)的流程繼續(xù)往下,輕量級(jí)鎖之后就會(huì)變成重量級(jí)鎖。首先我們先理解什么叫做輕量級(jí)鎖,從一個(gè)線程搶占資源(偏向鎖)到多線程搶占資源升級(jí)為輕量級(jí)鎖,線程如果沒(méi)那么多的話,其實(shí)這里就可以理解為CAS,也就是我們說(shuō)的Compare and Swap,比較并交換值。在并發(fā)編程中最簡(jiǎn)單的一個(gè)例子就是并發(fā)包下面的原子操作類AtomicInteger。在進(jìn)行類似++操作的時(shí)候,底層其實(shí)就是CAS鎖。
- public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1);}public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5;}
問(wèn)題4:什么情況下輕量級(jí)鎖要升級(jí)為重量級(jí)鎖呢?
首先我們可以思考的是多個(gè)線程的時(shí)候先開啟輕量級(jí)鎖,如果它c(diǎn)arry不了的情況下才會(huì)升級(jí)為重量級(jí)。那么什么情況下輕量級(jí)鎖會(huì)carry不住。1、如果線程數(shù)太多,比如上來(lái)就是10000個(gè),那么這里CAS要轉(zhuǎn)多久才可能交換值,同時(shí)CPU光在這10000個(gè)活著的線程中來(lái)回切換中就耗費(fèi)了巨大的資源,這種情況下自然就升級(jí)為重量級(jí)鎖,直接叫給操作系統(tǒng)入隊(duì)管理,那么就算10000個(gè)線程那也是處理休眠的情況等待排隊(duì)喚醒。
2、CAS如果自旋10次依然沒(méi)有獲取到鎖,那么也會(huì)升級(jí)為重量級(jí)。
總的來(lái)說(shuō)2種情況會(huì)從輕量級(jí)升級(jí)為重量級(jí),10次自旋或等待cpu調(diào)度的線程數(shù)超過(guò)cpu核數(shù)的一半,自動(dòng)升級(jí)為重量級(jí)鎖??捶?wù)器CPU的核數(shù)怎么看,輸入top指令,然后按1就可以看到。
問(wèn)題5:都說(shuō)syn為重量級(jí)鎖,那么到底重在哪里?
JVM偷懶把任何跟線程有關(guān)的操作全部交給操作系統(tǒng)去做,例如調(diào)度鎖的同步直接交給操作系統(tǒng)去執(zhí)行,而在操作系統(tǒng)中要執(zhí)行先要入隊(duì),另外操作系統(tǒng)啟動(dòng)一個(gè)線程時(shí)需要消耗很多資源,消耗資源比較重,重就重在這里。
整個(gè)鎖升級(jí)過(guò)程如圖所示:
四 synchronized的底層實(shí)現(xiàn)
上面我們對(duì)對(duì)象的內(nèi)存布局有了一些了解之后,知道鎖的狀態(tài)主要存放在markword里面。這里我們看看底層實(shí)現(xiàn)。
- public class RnEnterLockDemo { public void method() { synchronized (this) { System.out.println("start"); } }}
對(duì)這段簡(jiǎn)單代碼進(jìn)行反解析看看什么情況。javap -c RnEnterLockDemo.class
首先我們能確定的是syn肯定是還有加鎖的操作,看到的信息中出現(xiàn)了monitorenter和monitorexit,主觀上就可以猜到這是跟加鎖和解鎖相關(guān)的指令。有意思的是1個(gè)monitorenter和2個(gè)monitorexit。為什么呢?正常來(lái)說(shuō)應(yīng)該就是一個(gè)加鎖和一個(gè)釋放鎖啊。其實(shí)這里也體現(xiàn)了syn和lock的區(qū)別。syn是JVM層面的鎖,如果異常了不用自己釋放,jvm會(huì)自動(dòng)幫助釋放,這一步就取決于多出來(lái)的那個(gè)monitorexit。而lock異常需要我們手動(dòng)補(bǔ)獲并釋放的。
關(guān)于這兩條指令的作用,我們直接參考JVM規(guī)范中描述:
monitorenter :
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows: If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor. If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count. If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership
翻譯一下:
每個(gè)對(duì)象有一個(gè)監(jiān)視器鎖(monitor)。當(dāng)monitor被占用時(shí)就會(huì)處于鎖定狀態(tài),線程執(zhí)行monitorenter指令時(shí)嘗試獲取monitor的所有權(quán),過(guò)程如下:
如果monitor的進(jìn)入數(shù)為0,則該線程進(jìn)入monitor,然后將進(jìn)入數(shù)設(shè)置為1,該線程即為monitor的所有者。
如果線程已經(jīng)占有該monitor,只是重新進(jìn)入,則進(jìn)入monitor的進(jìn)入數(shù)加1。
如果其他線程已經(jīng)占用了monitor,則該線程進(jìn)入阻塞狀態(tài),直到monitor的進(jìn)入數(shù)為0,再重新嘗試獲取monitor的所有權(quán)。
monitorexit:
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref. The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
翻譯一下:
執(zhí)行monitorexit的線程必須是objectref所對(duì)應(yīng)的monitor的所有者。指令執(zhí)行時(shí),monitor的進(jìn)入數(shù)減1,如果減1后進(jìn)入數(shù)為0,那線程退出monitor,不再是這個(gè)monitor的所有者。其他被這個(gè)monitor阻塞的線程可以嘗試去獲取這個(gè) monitor的所有權(quán)。
通過(guò)這段話的描述,很清楚的看出Synchronized的實(shí)現(xiàn)原理,Synchronized底層通過(guò)一個(gè)monitor的對(duì)象來(lái)完成,wait/notify等方法其實(shí)也依賴于monitor對(duì)象,這就是為什么只有在同步的塊或者方法中才能調(diào)用wait/notify等方法,否則會(huì)拋出java.lang.IllegalMonitorStateException的異常。
每個(gè)鎖對(duì)象擁有一個(gè)鎖計(jì)數(shù)器和一個(gè)指向持有該鎖的線程的指針。
當(dāng)執(zhí)行monitorenter時(shí),如果目標(biāo)對(duì)象的計(jì)數(shù)器為零,那么說(shuō)明它沒(méi)有被其他線程所持有,Java虛擬機(jī)會(huì)將該鎖對(duì)象的持有線程設(shè)置為當(dāng)前線程,并且將其計(jì)數(shù)器加i。在目標(biāo)鎖對(duì)象的計(jì)數(shù)器不為零的情況下,如果鎖對(duì)象的持有線程是當(dāng)前線程,那么Java虛擬機(jī)可以將其計(jì)數(shù)器加1,否則需要等待,直至持有線程釋放該鎖。當(dāng)執(zhí)行monitorexit時(shí),Java虛擬機(jī)則需將鎖對(duì)象的計(jì)數(shù)器減1。計(jì)數(shù)器為零代表鎖已被釋放。
總結(jié)
以往的經(jīng)驗(yàn)中,只要用到synchronized就以為它已經(jīng)成為了重量級(jí)鎖。在jdk1.2之前確實(shí)如此,后來(lái)發(fā)現(xiàn)太重了,消耗了太多操作系統(tǒng)資源,所以對(duì)synchronized進(jìn)行了優(yōu)化。以后可以直接用,至于鎖的力度如何,JVM底層已經(jīng)做好了我們直接用就行。
最后再看看開頭的幾個(gè)問(wèn)題,是不是都理解了呢。帶著問(wèn)題去研究,往往會(huì)更加清晰。希望對(duì)大家有所幫助。