重量級(jí)鎖
前文解釋了synchronized的實(shí)現(xiàn)和運(yùn)用,了解monitor的作用,,但是由于monitor監(jiān)視器鎖的操作是基于操作系統(tǒng)的底層Mutex Lock實(shí)現(xiàn)的,,對(duì)所要加鎖線程加上互斥鎖,但是加鎖時(shí)間相比其他指令就長(zhǎng)很多了,,因此將這種基于互斥鎖的加鎖機(jī)制成為重量級(jí)鎖,。
而在JDK1.6之后,對(duì)synchronized優(yōu)化,,根據(jù)不同情形出現(xiàn)了偏向鎖,、輕量鎖、對(duì)象鎖,,自旋鎖(或自適應(yīng)自旋鎖)等,,因此,,現(xiàn)在的synchronized可以說是一個(gè)幾種鎖過程的封裝,。
為了更好地解釋下面幾種鎖,這里需要描述一下synchronized的線程排隊(duì)和鎖標(biāo)志位
對(duì)象頭
了解Java虛擬機(jī)知道Java的對(duì)象是創(chuàng)建在堆上的,,指向堆的引用才放在棧上,,而在堆上創(chuàng)建的對(duì)象結(jié)構(gòu)大致是這樣的
其中對(duì)象頭就是用于保存對(duì)象的信息,包括哈希碼(HashCode),、GC分代年齡,、鎖狀態(tài)標(biāo)志、線程持有的鎖,、偏向線程ID,、偏向時(shí)間戳等,在不同狀態(tài)的情況下,,對(duì)象頭內(nèi)容不同
這里我們主要關(guān)注的是鎖標(biāo)志位,,當(dāng)一個(gè)線程獲取對(duì)象并加鎖后,標(biāo)志位從01變?yōu)?0,,其他線程則處于排隊(duì)狀態(tài),。
圖中可以看出,當(dāng)存在調(diào)用對(duì)象被運(yùn)行的線程占用事,,請(qǐng)求線程會(huì)進(jìn)入排隊(duì)隊(duì)列,,還有wait之后notify的線程,。
自旋鎖(自適應(yīng)鎖)
由于線程阻塞后進(jìn)入排隊(duì)隊(duì)列和喚醒都需要CPU從用戶態(tài)轉(zhuǎn)為核心態(tài),花費(fèi)時(shí)間較多,,尤其頻繁的阻塞和喚醒對(duì)CPU來說也是負(fù)荷很重的工作,,同時(shí),統(tǒng)計(jì)發(fā)現(xiàn)很多線程鎖定狀態(tài)只持續(xù)很短時(shí)間,,如果這時(shí)候其他線程進(jìn)入等待隊(duì)列之后再喚醒太費(fèi)時(shí)間了,,因此,出現(xiàn)了自旋鎖,。
自旋鎖,,由于線程阻塞和喚醒的代價(jià)比較大,對(duì)于等待的線程,,不先加到等待隊(duì)列中,,而是去執(zhí)行一個(gè)無意義的循環(huán),一直到運(yùn)行的線程結(jié)束之后去競(jìng)爭(zhēng)鎖,。但是明顯自旋鎖使得synchronized的對(duì)象鎖方式在線程之間引入了不公平,,而且CPU在等待自旋鎖時(shí)不做任何有用的工作,僅僅是等待,,浪費(fèi)資源,,但是這樣可以保證大吞吐率和執(zhí)行效率。但是由于CPU的自旋消耗比較大,,因此自旋是有范圍的,,超過這個(gè)范圍就會(huì)進(jìn)入排隊(duì)隊(duì)列,即重量級(jí)鎖的機(jī)制
自適應(yīng)自旋鎖,,就是自旋的次數(shù)是通過JVM在運(yùn)行時(shí)收集的統(tǒng)計(jì)信息,,動(dòng)態(tài)調(diào)整自旋鎖的自旋次數(shù)上界。
輕量級(jí)鎖和偏向鎖
在某些情況下,,synchronized區(qū)域不存在競(jìng)爭(zhēng),,依然按照重量級(jí)鎖的方式運(yùn)行,會(huì)無端消耗資源,,因此JDK1.6之后,,加入了輕量鎖和偏向鎖。
不過,,需要注意的是輕量鎖或者偏向鎖不能代替重量級(jí)鎖的功能,,只是在無競(jìng)爭(zhēng)環(huán)境下,減少無端消耗,,如果出現(xiàn)競(jìng)爭(zhēng),,還是會(huì)轉(zhuǎn)換成重量級(jí)鎖。
輕量級(jí)鎖
在前面已經(jīng)描述了Java對(duì)象頭的結(jié)構(gòu),,對(duì)于除了鎖標(biāo)志位的部分,,被定義為Mark Word,,圖中可以發(fā)現(xiàn),不同鎖狀態(tài)的Mark Word內(nèi)容是不同,,這也是對(duì)Mark Word的運(yùn)用,,這里主要講無鎖狀態(tài)和輕量級(jí)鎖狀態(tài)的轉(zhuǎn)換。
加鎖
- 在運(yùn)行到同步塊的時(shí)候,,如果鎖標(biāo)志位為“01”狀態(tài),,是否偏向鎖為“0”(無鎖狀態(tài)),那么虛擬機(jī)當(dāng)前線程的棧幀中會(huì)建立一個(gè)名為鎖記錄(Lock Record)的空間,,用于存儲(chǔ)鎖對(duì)象目前的Mark Word的拷貝,,官方稱之為Displaced Mark Word。
- 拷貝對(duì)象頭中的Mark Word復(fù)制到鎖記錄中,,然后通過CAS操作嘗試將對(duì)象的Mark Word更新為指向Lock Record的指針,,將Lock record里的owner指針指向object mark word。CAS操作是用來保證多線程情況下操作原子性的方法,,即compare and set,,比較成功后再操作,這里就不詳細(xì)講了,。
- 如果更新成功,,那么線程獲取同步塊鎖并將對(duì)象頭的鎖標(biāo)志位改為00,操作如圖
- 如果失敗,,那么先檢查CAS操作是否成功,,如果成功,那么表示鎖已經(jīng)獲得,,直接執(zhí)行即可,,如果沒有,就說明有競(jìng)爭(zhēng),,那么就需要升級(jí)到重量級(jí)鎖。
整個(gè)流程可以由下圖表示
偏向鎖
偏向鎖是比輕量級(jí)鎖更輕量的鎖,,在無競(jìng)爭(zhēng)情況下,,使用輕量鎖還是需要CAS操作進(jìn)行信息交換等消耗一定資源,有的時(shí)候,,同步塊可能只被一個(gè)線程占用,,那么甚至不需要CAS交換信息,只要做標(biāo)志位即可,,偏向鎖就是這么做的,。
加鎖
- 當(dāng)同步塊的對(duì)象頭處于無鎖狀態(tài)時(shí),把對(duì)象頭中的標(biāo)志位設(shè)為“01”,,即偏向模式,。同時(shí)使用CAS操作把獲取到這個(gè)鎖的線程的ID記錄在對(duì)象的Mark Word之中的偏向線程ID,,并將是否偏向鎖的狀態(tài)位置置為1。
- 操作成功后持有偏向鎖的線程以后每次進(jìn)入這個(gè)鎖相關(guān)的同步塊時(shí),,直接檢查ThreadId是否和自身線程Id一致,。
- 如果一致,那么表示線程已經(jīng)獲取了鎖,,那么直接執(zhí)行即可,,不需要加鎖。
- 如果不一致,,表示有其他的線程訪問同步塊了,,此時(shí)需要判斷對(duì)象頭狀態(tài),如果此時(shí)處于無鎖狀態(tài),,那么就執(zhí)行最開始的操作,,將鎖轉(zhuǎn)移給該線程,如果仍然是偏向鎖狀態(tài),,那么轉(zhuǎn)變成輕量鎖,。
解鎖
- 線程不會(huì)主動(dòng)去釋放偏向鎖。偏向鎖的撤銷,,需要等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒有字節(jié)碼正在執(zhí)行),,它會(huì)首先暫停擁有偏向鎖的線程。
整個(gè)偏向鎖和升級(jí)輕量鎖的過程如圖
其他優(yōu)化
除了以上因?yàn)椴煌?jìng)爭(zhēng)狀態(tài)優(yōu)化的鎖,,還有一些因?yàn)樘厥鈶?yīng)用場(chǎng)景的優(yōu)化,。
鎖粗化
鎖粗化就是將多次連接在一起的加鎖、解鎖操作合并為一次,,將多個(gè)連續(xù)的鎖擴(kuò)展成一個(gè)范圍更大的鎖,。
StringBuffer str = new StringBuffer();
str.append("1");
str.append("2");
str.append("3");
這里由于StringBuffer內(nèi)部是有鎖的,,在執(zhí)行append()的時(shí)候原來是需要加鎖解鎖三次的,,這里鎖的粗化將三個(gè)鎖合并成一個(gè),最開始加鎖之后最后再解鎖,。
鎖消除
鎖消除就是虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí),,對(duì)一些代碼上要求同步,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行削除,,即對(duì)于代碼數(shù)據(jù)的逃逸分析,,如果數(shù)據(jù)無法逃逸并且私有的話,鎖其實(shí)是沒必要的,,可以消除,。
比如上面的StringBuffer變量,如果被放在有個(gè)私有函數(shù)中作為中間值,不被輸出,,那個(gè)根本不存在數(shù)據(jù)逃逸,,就不需要加鎖。
|