在多線程并發(fā)編程中Synchronized一直是元老級(jí)角色,很多人都會(huì)稱呼它為重量級(jí)鎖,但是隨著Java SE1.6對(duì)Synchronized進(jìn)行了各種優(yōu)化之后,有些情況下它并不那么重了,本文詳細(xì)介紹了Java SE1.6中為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗而引入的偏向鎖和輕量級(jí)鎖,以及鎖的存儲(chǔ)結(jié)構(gòu)和升級(jí)過(guò)程。
CAS(Compare and Swap),用于在硬件層面上提供原子性操作。在 Intel 處理器中,比較并交換通過(guò)指令cmpxchg實(shí)現(xiàn)。比較是否和給定的數(shù)值一致,如果一致則修改,不一致則不修改。
基礎(chǔ)
Java中的每一個(gè)對(duì)象都可以作為鎖。
- 對(duì)于同步方法,鎖是當(dāng)前實(shí)例對(duì)象。
- 對(duì)于靜態(tài)同步方法,鎖是當(dāng)前對(duì)象的Class對(duì)象。
- 對(duì)于同步方法塊,鎖是
Synchonized括號(hào)里配置的對(duì)象。
當(dāng)一個(gè)線程試圖訪問(wèn)同步代碼塊時(shí),它首先必須得到鎖,退出或拋出異常時(shí)必須釋放鎖。那么鎖存在哪里呢?鎖里面會(huì)存儲(chǔ)什么信息呢?
同步的原理
JVM規(guī)范規(guī)定JVM基于進(jìn)入和退出 Monitor 對(duì)象來(lái)實(shí)現(xiàn)方法同步和代碼塊同步,但兩者的實(shí)現(xiàn)細(xì)節(jié)不一樣。代碼塊同步是使用monitorenter和monitorexit指令實(shí)現(xiàn),而方法同步是使用另外一種方式實(shí)現(xiàn)的,細(xì)節(jié)在JVM規(guī)范里并沒(méi)有詳細(xì)說(shuō)明,但是方法的同步同樣可以使用這兩個(gè)指令來(lái)實(shí)現(xiàn)。
monitorenter指令是在編譯后插入到同步代碼塊的開(kāi)始位置,而monitorexit是插入到方法結(jié)束處和異常處,JVM要保證每個(gè)monitorenter必須有對(duì)應(yīng)的monitorexit與之配對(duì)。任何對(duì)象都有一個(gè)monitor與之關(guān)聯(lián),當(dāng)且一個(gè)monitor被持有后,它將處于鎖定狀態(tài)。線程執(zhí)行到 monitorenter 指令時(shí),將會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的 monitor 的所有權(quán),即嘗試獲得對(duì)象的鎖。
Java對(duì)象頭
鎖存在Java對(duì)象頭里。如果對(duì)象是數(shù)組類型,則虛擬機(jī)用3個(gè)Word(字寬)存儲(chǔ)對(duì)象頭,如果對(duì)象是非數(shù)組類型,則用2字寬存儲(chǔ)對(duì)象頭。在32位虛擬機(jī)中,一字寬等于四字節(jié),即32bit。
| 長(zhǎng)度 | 內(nèi)容 | 說(shuō)明 |
| :------------- | :------------- |
|32/64bit| Mark Word |存儲(chǔ)對(duì)象的hashCode或鎖信息等|
|32/64bit| Class Metadata Address |存儲(chǔ)到對(duì)象類型數(shù)據(jù)的指針|
|32/64bit| Array length |數(shù)組的長(zhǎng)度(如果當(dāng)前對(duì)象是數(shù)組)|
Java對(duì)象頭里的Mark Word里默認(rèn)存儲(chǔ)對(duì)象的HashCode,分代年齡和鎖標(biāo)記位。32位JVM的Mark Word的默認(rèn)存儲(chǔ)結(jié)構(gòu)如下:
| | 25 bit |4bit|1bit是否是偏向鎖|2bit鎖標(biāo)志位|
| :------------- | :------------- |
|無(wú)鎖狀態(tài)| 對(duì)象的hashCode| 對(duì)象分代年齡| 0| 01|
在運(yùn)行期間Mark Word里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化。Mark Word可能變化為存儲(chǔ)以下4種數(shù)據(jù):

鎖的升級(jí)
Java SE1.6為了減少獲得鎖和釋放鎖所帶來(lái)的性能消耗,引入了“偏向鎖”和“輕量級(jí)鎖”,所以在Java SE1.6里鎖一共有四種狀態(tài),無(wú)鎖狀態(tài),偏向鎖狀態(tài),輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài),它會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí)。鎖可以升級(jí)但不能降級(jí),意味著偏向鎖升級(jí)成輕量級(jí)鎖后不能降級(jí)成偏向鎖。這種鎖升級(jí)卻不能降級(jí)的策略,目的是為了提高獲得鎖和釋放鎖的效率。\
線程對(duì)鎖的競(jìng)爭(zhēng)
當(dāng)多個(gè)線程同時(shí)請(qǐng)求某個(gè)對(duì)象監(jiān)視器時(shí),對(duì)象監(jiān)視器會(huì)設(shè)置幾種狀態(tài)用來(lái)區(qū)分請(qǐng)求的線程:
-
Contention List:所有請(qǐng)求鎖的線程將被首先放置到該競(jìng)爭(zhēng)隊(duì)列 -
Entry List:Contention List中那些有資格成為候選人的線程被移到Entry List -
Wait Set:那些調(diào)用wait方法被阻塞的線程被放置到Wait Set -
OnDeck:任何時(shí)刻最多只能有一個(gè)線程正在競(jìng)爭(zhēng)鎖,該線程稱為OnDeck -
Owner:獲得鎖的線程稱為Owner -
!Owner:釋放鎖的線程

新請(qǐng)求鎖的線程將首先被加入到ConetentionList中,當(dāng)某個(gè)擁有鎖的線程(Owner狀態(tài))調(diào)用unlock之后,如果發(fā)現(xiàn)EntryList為空則從ContentionList中移動(dòng)線程到EntryList,下面說(shuō)明下ContentionList和EntryList的實(shí)現(xiàn)方式:
ContentionList
ContentionList并不是一個(gè)真正的Queue,而只是一個(gè)虛擬隊(duì)列,原因在于ContentionList是由Node及其next指針邏輯構(gòu)成,并不存在一個(gè)Queue的數(shù)據(jù)結(jié)構(gòu)。ContentionList是一個(gè)后進(jìn)先出(LIFO)的隊(duì)列,每次新加入Node時(shí)都會(huì)在隊(duì)頭進(jìn)行,通過(guò)CAS改變第一個(gè)節(jié)點(diǎn)的的指針為新增節(jié)點(diǎn),同時(shí)設(shè)置新增節(jié)點(diǎn)的next指向后續(xù)節(jié)點(diǎn),而取得操作則發(fā)生在隊(duì)尾。顯然,該結(jié)構(gòu)其實(shí)是個(gè)Lock-Free的隊(duì)列。
因?yàn)橹挥蠴wner線程才能從隊(duì)尾取元素,也即線程出列操作無(wú)爭(zhēng)用,當(dāng)然也就避免了CAS的ABA問(wèn)題。
EntryList
EntryList與ContentionList邏輯上同屬等待隊(duì)列,ContentionList會(huì)被線程并發(fā)訪問(wèn),為了降低對(duì)ContentionList隊(duì)尾的爭(zhēng)用,而建立EntryList。Owner線程在unlock時(shí)會(huì)從ContentionList中遷移線程到EntryList,并會(huì)指定EntryList中的某個(gè)線程(一般為Head)為Ready(OnDeck)線程。Owner線程并不是把鎖傳遞給OnDeck線程,只是把競(jìng)爭(zhēng)鎖的權(quán)利交給OnDeck,OnDeck線程需要重新競(jìng)爭(zhēng)鎖。這樣做雖然犧牲了一定的公平性,但極大的提高了整體吞吐量,在Hotspot中把OnDeck的選擇行為稱之為“競(jìng)爭(zhēng)切換”。
OnDeck線程獲得鎖后即變?yōu)閛wner線程,無(wú)法獲得鎖則會(huì)依然留在EntryList中,考慮到公平性,在EntryList中的位置不發(fā)生變化(依然在隊(duì)頭)。如果Owner線程被wait方法阻塞,則轉(zhuǎn)移到WaitSet隊(duì)列;如果在某個(gè)時(shí)刻被notify/notifyAll喚醒,則再次轉(zhuǎn)移到EntryList。