sychronized的實(shí)現(xiàn)原理與應(yīng)用
在多線程并發(fā)編程中synchronized一直是元老的角色,很多人都會稱呼它為重量級鎖串绩。但是,隨著Java SE 1.6對synchronized進(jìn)行了各種優(yōu)化后狐史,有很多特殊情況下它就并不是那個重了评凝。本文詳細(xì)介紹Java SE 1.6 中為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖,以及鎖的升級過程外邓。
sychronized鎖的基礎(chǔ)
Java中的每一個對象都可以作為鎖撤蚊。具體可以有以下三種形式
- 1.對于普通的同步方法古掏,鎖的是當(dāng)前實(shí)例對象损话。
- 2.對于靜態(tài)同步方法,鎖是當(dāng)前的Class對象槽唾。
- 3.對于同步方法塊丧枪,鎖是Synchonized括號里配置的對象。
當(dāng)一個線程識圖訪問同步代碼塊時庞萍,它首先必須得到鎖拧烦,退出或者拋出異常的時候必須釋放鎖。
從JVM規(guī)范中可以看到synchronized在JVM里的實(shí)現(xiàn)原理钝计。JVM基于進(jìn)入和退出Monitor對象來實(shí)現(xiàn)方法同步和代碼塊同步恋博,但兩者的實(shí)現(xiàn)細(xì)節(jié)不一樣齐佳。代碼塊的同步是使用monitorenter和monitorexit指令實(shí)現(xiàn)的,而方法的同步使用修飾符上的ACC_SYNCHNIZED完成的债沮。方法的同步同樣可以使用這兩個指令完成炼吴。不管使用哪一種,本質(zhì)是對一個對象的監(jiān)視器進(jìn)行獲取疫衩,同一個時刻只能有一個線程獲取到由synchronized保護(hù)對象的監(jiān)視器硅蹦。
monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法的結(jié)束處和異常處闷煤。任何對象都有一個monitor與之關(guān)聯(lián)童芹,當(dāng)且一個monitor被持有后,它將處于鎖定狀態(tài)鲤拿。線程執(zhí)行到monitorenter指令時假褪,將會嘗試獲取對象所對應(yīng)的monitor所有權(quán),即嘗試獲取對象的鎖皆愉。
Java對象頭
synchronized用的鎖是存在java對象頭里的嗜价。如果對象是數(shù)組類型,則虛擬機(jī)用3個字節(jié)寬(word)存儲對象頭幕庐,如果是非數(shù)組類型久锥,則用2個字節(jié)寬存儲。32位虛擬機(jī)1字寬=4字節(jié)=32bit
對象頭內(nèi)容包括MarkWord(32/64bit 存儲對象的hashCode或鎖信息)异剥、Class Metadata Address(32/64bit 存儲對象類型數(shù)據(jù)的指針)瑟由,Array Length(32/32bit 數(shù)組長度)
java對象頭里的Mark Word里默認(rèn)存儲對象的HashCode、分代年齡和鎖標(biāo)記位冤寿。32位JVM的Mark Word可能變化存儲為以下5種數(shù)據(jù):
在64位虛擬機(jī)下歹苦,Mark Word是64bit大小,其存儲結(jié)構(gòu)如下
鎖升級對比
Java SE 1.6 為了減少獲得鎖和釋放鎖帶來的性能消耗督怜,引入了“偏向鎖”和“輕量級鎖”殴瘦。鎖一共有四種狀態(tài):無鎖狀態(tài)、偏向鎖号杠、輕量級鎖蚪腋、重量級鎖,這個狀態(tài)隨著競爭情況主鍵升級姨蟋。鎖可以升級但不可以降級屉凯。不可逆這是為了提供獲得鎖和釋放鎖的效率。
先介紹一下自旋鎖
自旋鎖
引入背景
在多線程競爭鎖時眼溶,當(dāng)一個線程獲取鎖時悠砚,它會阻塞所有正在競爭的線程,這樣對性能帶來了極大的影響堂飞。在掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成灌旧,這些操作個i系統(tǒng)的并發(fā)性能帶來了很大的壓力绑咱。同時HotSpot團(tuán)隊(duì)注意到在很多情況下,共享數(shù)據(jù)的鎖定狀態(tài)只會持續(xù)很短的一段時間枢泰,為了這段時間去掛起和回復(fù)阻塞線程并不值得羡玛。在如今多處理器環(huán)境下,完全可以讓另一個沒有獲取到鎖的線程在門外等待一會(自旋)宗苍,但不放棄CPU的執(zhí)行時間稼稿。等待持有鎖的線程是否很快就會釋放鎖。為了讓線程等待讳窟,我們只需要讓線程執(zhí)行一個忙循環(huán)(自旋)让歼,這便是自旋鎖由來的原因。優(yōu)化
自旋鎖早在JDK1.4 中就引入了丽啡,只是當(dāng)時默認(rèn)時關(guān)閉的谋右。在JDK 1.6后默認(rèn)為開啟狀態(tài)。自旋鎖本質(zhì)上與阻塞并不相同补箍,先不考慮其對多處理器的要求改执,如果鎖占用的時間非常的短,那么自旋鎖的新能會非常的好坑雅,相反辈挂,其會帶來更多的性能開銷(因?yàn)樵诰€程自旋時,始終會占用CPU的時間片裹粤,如果鎖占用的時間太長终蒂,那么自旋的線程會白白消耗掉CPU資源)。因此自旋等待的時間必須要有一定的限度遥诉,如果自選超過了限定的次數(shù)仍然沒有成功獲取到鎖拇泣,就應(yīng)該使用傳統(tǒng)的方式去掛起線程了,在JDK定義中矮锈,自旋鎖默認(rèn)的自旋次數(shù)為10次霉翔,用戶可以使用參數(shù)-XX:PreBlockSpin來更改。
偏向鎖
在大多實(shí)際環(huán)境下苞笨,鎖不僅不存在多線程競爭债朵,而且總是由同一個線程多次獲取,那么在同一個線程反復(fù)獲取所釋放鎖中猫缭,其中并還沒有鎖的競爭葱弟,那么這樣看上去壹店,多次的獲取鎖和釋放鎖帶來了很多不必要的性能開銷和上下文切換猜丹。
為了解決這一問題,HotSpot的作者在Java SE 1.6 中對Synchronized進(jìn)行了優(yōu)化硅卢,引入了偏向鎖射窒。當(dāng)一個線程訪問同步快并獲取鎖時藏杖,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進(jìn)入和推出同步塊時不需要進(jìn)行CAS操作來加鎖和解鎖脉顿。只需要簡單地測試一下對象頭的Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖蝌麸。如果成功,表示線程已經(jīng)獲取到了鎖艾疟。
-
偏向鎖的撤銷
偏向鎖使用了一種等到競爭出現(xiàn)才釋放鎖的機(jī)制来吩,所以當(dāng)其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖蔽莱。偏向鎖的撤銷需要等待擁有偏向鎖的線程到達(dá)全局安全點(diǎn)(在這個時間點(diǎn)上沒有字節(jié)碼正在執(zhí)行)弟疆,會首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著盗冷,如果線程不處于活動狀態(tài)怠苔,則將鎖的對象的對象頭設(shè)置成無鎖狀態(tài),如果線程仍然活著仪糖,擁有偏向鎖的棧會被執(zhí)行(判斷是否需要持有鎖)柑司,遍歷偏向?qū)ο蟮逆i記錄,查看使用情況锅劝,如果還需要持有偏向鎖攒驰,則偏向鎖升級為輕量級鎖,如果不需要持有偏向鎖了故爵,則將鎖對象恢復(fù)成無鎖狀態(tài)讼育,最后喚醒暫停的線程。
輕量級鎖
加鎖
線程在執(zhí)行同步塊之前稠集,JVM會現(xiàn)在當(dāng)前線程的棧幀中創(chuàng)建用于存儲鎖記錄的空間奶段,并將對象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Disolaced Mard Word剥纷。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針痹籍。如果成功,當(dāng)前線程獲得鎖晦鞋,如果失敗蹲缠,表示其他線程競爭,當(dāng)前線程便嘗試使用自選來獲取鎖悠垛。解鎖
輕量級解鎖時线定,會使用原子的CAS操作將Displaced Mard Word替換回到對象頭,如果成功确买,則表示沒有競爭發(fā)生斤讥。如果失敗,表示當(dāng)前鎖存在競爭湾趾,鎖就會膨脹成重量級鎖芭商。