接觸過線程安全的同學(xué)想必都使用過synchronized這個關(guān)鍵字凭需,在java同步代碼快中,synchronized的使用方式無非有兩個:
- 通過對一個對象進(jìn)行加鎖來實現(xiàn)同步,如下面代碼救拉。
synchronized(lockObject){
//代碼
}
- 對一個方法進(jìn)行synchronized聲明氏堤,進(jìn)而對一個方法進(jìn)行加鎖來實現(xiàn)同步档插。如下面代碼
public synchornized void test(){
//代碼
}
但這里需要指出的是慢蜓,無論是對一個對象進(jìn)行加鎖還是對一個方法進(jìn)行加鎖,實際上郭膛,都是對對象進(jìn)行加鎖晨抡。
也就是說,對于方式2则剃,實際上虛擬機會根據(jù)synchronized修飾的是實例方法還是類方法耘柱,去取對應(yīng)的實例對象或者Class對象來進(jìn)行加鎖。
對于synchronized這個關(guān)鍵字棍现,可能之前大家有聽過调煎,他是一個重量級鎖,開銷很大己肮,建議大家少用點士袄。但大家可能也聽說過,但到了jdk1.6之后谎僻,該關(guān)鍵字被進(jìn)行了很多的優(yōu)化娄柳,已經(jīng)不像以前那樣不給力了,建議大家多使用戈稿。
那么它是進(jìn)行了什么樣的優(yōu)化西土,才使得synchronized又深得人心呢?為何重量級鎖開銷就大呢鞍盗?
想必大家也都聽說過輕量級鎖需了,重量級鎖,自旋鎖般甲,自適應(yīng)自旋鎖肋乍,偏向鎖等等,他們都有哪些區(qū)別呢敷存?
剛才和大家說墓造,鎖是加在對象上的,那么一個線程是如何知道這個對象被加了鎖呢锚烦?又是如何知道它加的是什么類型的鎖呢觅闽?
基于這些問題,下面我講一步一步講解synchronized是如何被優(yōu)化的涮俄,是如何從偏向鎖到重量級鎖的蛉拙。
鎖對象
剛才我們說,鎖實際上是加在對象上的彻亲,那么被加了鎖的對象我們稱之為鎖對象孕锄,在java中吮廉,任何一個對象都能成為鎖對象。
為了讓大家更好著理解虛擬機是如何知道這個對象就是一個鎖對象的畸肆,我們下面簡單介紹一下java中一個對象的結(jié)構(gòu)宦芦。
java對象在內(nèi)存中的存儲結(jié)構(gòu)主要有一下三個部分:
- 對象頭
- 實例數(shù)據(jù)
- 填充數(shù)據(jù)
這里強調(diào)一下,對象頭里的數(shù)據(jù)主要是一些運行時的數(shù)據(jù)轴脐。
其簡單的結(jié)構(gòu)如下
長度 | 內(nèi)容 | 說明 |
---|---|---|
32/64bit | Mark Work | hashCode,GC分代年齡调卑,鎖信息 |
32/64bit | Class Metadata Address | 指向?qū)ο箢愋蛿?shù)據(jù)的指針 |
32/64bit | Array Length | 數(shù)組的長度(當(dāng)對象為數(shù)組時) |
從該表格中我們可以看到,對象中關(guān)于鎖的信息是存在Markword里的大咱。
我們來看一段代碼
LockObject lockObject = new LockObject();//隨便創(chuàng)建一個對象
synchronized(lockObject){
//代碼
}
當(dāng)我們創(chuàng)建一個對象LockObject時令野,該對象的部分Markword關(guān)鍵數(shù)據(jù)如下。
bit fields | 是否偏向鎖 | 鎖標(biāo)志位 |
---|---|---|
hash | 0 | 01 |
從圖中可以看出徽级,偏向鎖的標(biāo)志位是“01”,狀態(tài)是“0”聊浅,表示該對象還沒有被加上偏向鎖餐抢。(“1”是表示被加上偏向鎖)。該對象被創(chuàng)建出來的那一刻低匙,就有了偏向鎖的標(biāo)志位旷痕,這也說明了所有對象都是可偏向的,但所有對象的狀態(tài)都為“0”顽冶,也同時說明所有被創(chuàng)建的對象的偏向鎖并沒有生效欺抗。
偏向鎖
不過,當(dāng)線程執(zhí)行到臨界區(qū)(critical section)時强重,此時會利用CAS(Compare and Swap)操作绞呈,將線程ID插入到Markword中,同時修改偏向鎖的標(biāo)志位间景。
所謂臨界區(qū)佃声,就是只允許一個線程進(jìn)去執(zhí)行操作的區(qū)域,即同步代碼塊倘要。CAS是一個原子性操作
此時的Mark word的結(jié)構(gòu)信息如下:
bit fields | 是否偏向鎖 | 鎖標(biāo)志位 | |
---|---|---|---|
threadId | epoch | 1 | 01 |
此時偏向鎖的狀態(tài)為“1”圾亏,說明對象的偏向鎖生效了,同時也可以看到封拧,哪個線程獲得了該對象的鎖志鹃。
那么,什么是偏向鎖?
偏向鎖是jdk1.6引入的一項鎖優(yōu)化泽西,其中的“偏”是偏心的偏曹铃。它的意思就是說,這個鎖會偏向于第一個獲得它的線程尝苇,在接下來的執(zhí)行過程中铛只,假如該鎖沒有被其他線程所獲取埠胖,沒有其他線程來競爭該鎖,那么持有偏向鎖的線程將永遠(yuǎn)不需要進(jìn)行同步操作淳玩。
也就是說:
在此線程之后的執(zhí)行過程中直撤,如果再次進(jìn)入或者退出同一段同步塊代碼,并不再需要去進(jìn)行加鎖或者解鎖操作蜕着,而是會做以下的步驟:
- Load-and-test谋竖,也就是簡單判斷一下當(dāng)前線程id是否與Markword當(dāng)中的線程id是否一致.
- 如果一致,則說明此線程已經(jīng)成功獲得了鎖承匣,繼續(xù)執(zhí)行下面的代碼.
- 如果不一致蓖乘,則要檢查一下對象是否還是可偏向,即“是否偏向鎖”標(biāo)志位的值韧骗。
- 如果還未偏向嘉抒,則利用CAS操作來競爭鎖,也即是第一次獲取鎖時的操作袍暴。
如果此對象已經(jīng)偏向了些侍,并且不是偏向自己,則說明存在了競爭政模。此時可能就要根據(jù)另外線程的情況岗宣,可能是重新偏向,也有可能是做偏向撤銷淋样,但大部分情況下就是升級成輕量級鎖了耗式。
可以看出,偏向鎖是針對于一個線程而言的趁猴,線程獲得鎖之后就不會再有解鎖等操作了刊咳,這樣可以省略很多開銷。假如有兩個線程來競爭該鎖話躲叼,那么偏向鎖就失效了芦缰,進(jìn)而升級成輕量級鎖了。
為什么要這樣做呢枫慷?因為經(jīng)驗表明让蕾,其實大部分情況下,都會是同一個線程進(jìn)入同一塊同步代碼塊的或听。這也是為什么會有偏向鎖出現(xiàn)的原因探孝。
在Jdk1.6中,偏向鎖的開關(guān)是默認(rèn)開啟的誉裆,適用于只有一個線程訪問同步塊的場景顿颅。
鎖膨脹
剛才說了,當(dāng)出現(xiàn)有兩個線程來競爭鎖的話足丢,那么偏向鎖就失效了粱腻,此時鎖就會膨脹庇配,升級為輕量級鎖。這也是我們經(jīng)常所說的鎖膨脹
鎖撤銷
由于偏向鎖失效了绍些,那么接下來就得把該鎖撤銷捞慌,鎖撤銷的開銷花費還是挺大的,其大概的過程如下:
- 在一個安全點停止擁有鎖的線程柬批。
- 遍歷線程棧啸澡,如果存在鎖記錄的話,需要修復(fù)鎖記錄和Markword氮帐,使其變成無鎖狀態(tài)嗅虏。
- 喚醒當(dāng)前線程,將當(dāng)前鎖升級成輕量級鎖上沐。
所以皮服,如果某些同步代碼塊大多數(shù)情況下都是有兩個及以上的線程競爭的話,那么偏向鎖就會是一種累贅参咙,對于這種情況冰更,我們可以一開始就把偏向鎖這個默認(rèn)功能給關(guān)閉
輕量級鎖
鎖撤銷升級為輕量級鎖之后,那么對象的Markword也會進(jìn)行相應(yīng)的的變化昂勒。下面先簡單描述下鎖撤銷之后,升級為輕量級鎖的過程:
- 線程在自己的棧楨中創(chuàng)建鎖記錄 LockRecord舟铜。
- 將鎖對象的對象頭中的MarkWord復(fù)制到線程的剛剛創(chuàng)建的鎖記錄中戈盈。
- 將鎖記錄中的Owner指針指向鎖對象。
- 將鎖對象的對象頭的MarkWord替換為指向鎖記錄的指針谆刨。
對應(yīng)的圖描述如下(圖來自周志明深入java虛擬機)
之后Markwork如下:
bit fields | 鎖標(biāo)志位 |
---|---|
指向LockRecord的指針 | 00 |
注:鎖標(biāo)志位"00"表示輕量級鎖
輕量級鎖主要有兩種
- 自旋鎖
- 自適應(yīng)自旋鎖
自旋鎖
所謂自旋塘娶,就是指當(dāng)有另外一個線程來競爭鎖時,這個線程會在原地循環(huán)等待痊夭,而不是把該線程給阻塞刁岸,直到那個獲得鎖的線程釋放鎖之后,這個線程就可以馬上獲得鎖的她我。
注意虹曙,鎖在原地循環(huán)的時候,是會消耗cpu的番舆,就相當(dāng)于在執(zhí)行一個啥也沒有的for循環(huán)酝碳。
所以,輕量級鎖適用于那些同步代碼塊執(zhí)行的很快的場景恨狈,這樣疏哗,線程原地等待很短很短的時間就能夠獲得鎖了。
經(jīng)驗表明禾怠,大部分同步代碼塊執(zhí)行的時間都是很短很短的返奉,也正是基于這個原因贝搁,才有了輕量級鎖這么個東西。
自旋鎖的一些問題
- 如果同步代碼塊執(zhí)行的很慢芽偏,需要消耗大量的時間雷逆,那么這個時侯,其他線程在原地等待空消耗cpu哮针,這會讓人很難受关面。
- 本來一個線程把鎖釋放之后,當(dāng)前線程是能夠獲得鎖的十厢,但是假如這個時候有好幾個線程都在競爭這個鎖的話等太,那么有可能當(dāng)前線程會獲取不到鎖,還得原地等待繼續(xù)空循環(huán)消耗cup蛮放,甚至有可能一直獲取不到鎖缩抡。
基于這個問題,我們必須給線程空循環(huán)設(shè)置一個次數(shù)包颁,當(dāng)線程超過了這個次數(shù)瞻想,我們就認(rèn)為,繼續(xù)使用自旋鎖就不適合了娩嚼,此時鎖會再次膨脹蘑险,升級為重量級鎖。
默認(rèn)情況下岳悟,自旋的次數(shù)為10次佃迄,用戶可以通過-XX:PreBlockSpin來進(jìn)行更改。
自旋鎖是在JDK1.4.2的時候引入的
自適應(yīng)自旋鎖
所謂自適應(yīng)自旋鎖就是線程空循環(huán)等待的自旋次數(shù)并非是固定的贵少,而是會動態(tài)著根據(jù)實際情況來改變自旋等待的次數(shù)呵俏。
其大概原理是這樣的:
假如一個線程1剛剛成功獲得一個鎖,當(dāng)它把鎖釋放了之后滔灶,線程2獲得該鎖普碎,并且線程2在運行的過程中,此時線程1又想來獲得該鎖了录平,但線程2還沒有釋放該鎖麻车,所以線程1只能自旋等待,但是虛擬機認(rèn)為斗这,由于線程1剛剛獲得過該鎖绪氛,那么虛擬機覺得線程1這次自旋也是很有可能能夠再次成功獲得該鎖的,所以會延長線程1自旋的次數(shù)涝影。
另外枣察,如果對于某一個鎖,一個線程自旋之后,很少成功獲得該鎖序目,那么以后這個線程要獲取該鎖時臂痕,是有可能直接忽略掉自旋過程,直接升級為重量級鎖的猿涨,以免空循環(huán)等待浪費資源握童。
輕量級鎖也被稱為非阻塞同步、樂觀鎖叛赚,因為這個過程并沒有把線程阻塞掛起澡绩,而是讓線程空循環(huán)等待,串行執(zhí)行俺附。
重量級鎖
輕量級鎖膨脹之后肥卡,就升級為重量級鎖了。重量級鎖是依賴對象內(nèi)部的monitor鎖來實現(xiàn)的事镣,而monitor又依賴操作系統(tǒng)的MutexLock(互斥鎖)來實現(xiàn)的步鉴,所以重量級鎖也被成為互斥鎖。
當(dāng)輕量級所經(jīng)過鎖撤銷等步驟升級為重量級鎖之后璃哟,它的Markword部分?jǐn)?shù)據(jù)大體如下
bit fields | 鎖標(biāo)志位 |
---|---|
指向Mutex的指針 | 10 |
為什么說重量級鎖開銷大呢
主要是氛琢,當(dāng)系統(tǒng)檢查到鎖是重量級鎖之后,會把等待想要獲得鎖的線程進(jìn)行阻塞随闪,被阻塞的線程不會消耗cup阳似。但是阻塞或者喚醒一個線程時,都需要操作系統(tǒng)來幫忙铐伴,這就需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài)障般,而轉(zhuǎn)換狀態(tài)是需要消耗很多時間的,有可能比用戶執(zhí)行代碼的時間還要長盛杰。
這就是說為什么重量級線程開銷很大的。
互斥鎖(重量級鎖)也稱為阻塞同步藐石、悲觀鎖
總結(jié)
通過上面的分析即供,我們知道了為什么synchronized關(guān)鍵字為何又深得人心,也知道了鎖的演變過程于微。
也就是說逗嫡,synchronized關(guān)鍵字并非一開始就該對象加上重量級鎖,也是從偏向鎖株依,輕量級鎖驱证,再到重量級鎖的過程。
這個過程也告訴我們恋腕,假如我們一開始就知道某個同步代碼塊的競爭很激烈抹锄、很慢的話,那么我們一開始就應(yīng)該使用重量級鎖了,從而省掉一些鎖轉(zhuǎn)換的開銷伙单。
講到這里就大概完了获高,希望能對你有所幫助
完
參考資料
- 深入理解java虛擬機(JVM高級特性與最佳實踐)
- java并發(fā)編程
- Eliminating Synchronization Related Atomic Operations with Biased Locking and Bulk Rebiasing
關(guān)注我的公眾號:苦逼的碼農(nóng),獲取更多原創(chuàng)文章吻育,后臺回復(fù)"禮包"送你一份特別的資源大禮包念秧。