在多線程并發(fā)編程中Synchronized
一直是元老級(jí)角色凤粗,很多人都會(huì)稱(chēng)呼它為重量級(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ù)組類(lèi)型督禽,則虛擬機(jī)用3個(gè)Word(字寬)存儲(chǔ)對(duì)象頭脆霎,如果對(duì)象是非數(shù)組類(lèi)型,則用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ì)象類(lèi)型數(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)鎖蚜迅,該線程稱(chēng)為OnDeck -
Owner
:獲得鎖的線程稱(chēng)為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ì)列,原因在于ContentionLis
t是由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的選擇行為稱(chēng)之為“競(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
篮赢。