每一個(gè)剛接觸多線程并發(fā)編程的同學(xué)破托,當(dāng)被問(wèn)到肪跋,如果多個(gè)線程同時(shí)訪問(wèn)一段代碼,發(fā)生并發(fā)的時(shí)候炼团,應(yīng)該怎么處理澎嚣?
我相信閃現(xiàn)在腦海中的第一個(gè)解決方案就是用synchronized,用鎖瘟芝,讓這段代碼同一時(shí)間只能被一個(gè)線程執(zhí)行易桃。
我們也知道,synchronized關(guān)鍵字可以用在方法上锌俱,也可以用在代碼塊上晤郑,如果要使用synchronized,我們一般就會(huì)如下使用:
public synchronized void doSomething() {
//do something here
}
或者
synchronized(LockObject) {
//do something here
}
那么實(shí)際上贸宏,synchronized關(guān)鍵字到底是怎么加鎖的造寝?鎖又長(zhǎng)什么樣子的呢?關(guān)于鎖吭练,還有一些什么樣的概念需要我們?nèi)フJ(rèn)識(shí)诫龙,去學(xué)習(xí),去理解的呢鲫咽?
以前在學(xué)習(xí)synchronized的時(shí)候签赃,就有文章說(shuō), synchronized是一個(gè)很重的操作,開(kāi)銷很大分尸,不要輕易使用锦聊,我們接受了這樣的觀點(diǎn),但是為什么說(shuō)是重的操作呢箩绍,為什么開(kāi)銷就大呢孔庭?
到j(luò)ava 1.6之后,java的開(kāi)發(fā)人員又針對(duì)鎖機(jī)制實(shí)現(xiàn)了一些優(yōu)化材蛛,又有文章告訴我們現(xiàn)在經(jīng)過(guò)優(yōu)化后圆到,使用synchronized并沒(méi)有什么太大的問(wèn)題了,那這又是因?yàn)槭裁丛蚰乇翱裕康降资亲隽耸裁磧?yōu)化构资?
那今天我們就嘗試著從鎖機(jī)制實(shí)現(xiàn)的角度,來(lái)講述一下synchronized在java虛擬機(jī)上面的適應(yīng)場(chǎng)景是怎么樣的陨簇。
由于java在1.6之后吐绵,引入了一些優(yōu)化的方案迹淌,所以我們講述synchronized,也會(huì)基于java1.6之后的版本己单。
鎖對(duì)象
首先唉窃,我們要知道鎖其實(shí)就是一個(gè)對(duì)象,java中每一個(gè)對(duì)象都能夠作為鎖纹笼。
所以我們?cè)谑褂胹ynchronized的時(shí)候纹份,
- 對(duì)于同步代碼塊,就得指定鎖對(duì)象廷痘。
- 對(duì)于修飾方法的synchronized蔓涧,默認(rèn)的鎖對(duì)象就是當(dāng)前方法的對(duì)象。
- 對(duì)于修飾靜態(tài)方法的synchronized笋额,其鎖對(duì)象就是此方法所對(duì)應(yīng)的類Class對(duì)象元暴。
我們知道,所謂的對(duì)象兄猩,無(wú)非也就是內(nèi)存上的一段地址茉盏,上面存放著對(duì)應(yīng)的數(shù)據(jù),那么我們就要想枢冤,作為鎖鸠姨,它跟其它的對(duì)象有什么不一樣呢?怎么知道這個(gè)對(duì)象就是鎖呢淹真?怎么知道它跟哪個(gè)線程關(guān)聯(lián)呢讶迁?它又怎么能夠控制線程對(duì)于同步代碼塊的訪問(wèn)呢蜗侈?
Markword
可以了解到在虛擬機(jī)中允蜈,對(duì)象在內(nèi)存中的存儲(chǔ)分為三部分:
- 對(duì)象頭
- 實(shí)例數(shù)據(jù)
3 對(duì)齊填充
其中,對(duì)象頭填充的是該對(duì)象的一些運(yùn)行時(shí)數(shù)據(jù)痢士,虛擬機(jī)一般用2到3個(gè)字寬來(lái)存儲(chǔ)對(duì)象頭值纱。
- 數(shù)組對(duì)象,會(huì)用3個(gè)字寬來(lái)存儲(chǔ)坯汤。
- 非數(shù)據(jù)對(duì)象虐唠,則用2個(gè)字寬來(lái)存儲(chǔ)。
其結(jié)構(gòu)簡(jiǎn)單如下:
長(zhǎng)度 | 內(nèi)容 | 說(shuō)明 |
---|---|---|
32/64bit | Markword | hashCode惰聂,GC分代年齡疆偿,鎖信息 |
32/64bit | Class Metadata Address | 指向?qū)ο箢愋蛿?shù)據(jù)的指針 |
32/64bit | Array Length | 數(shù)組的長(zhǎng)度(當(dāng)對(duì)象為數(shù)組時(shí)) |
從上表中,我們可以看到搓幌,鎖相關(guān)的信息杆故,是存在稱之為Markword中的內(nèi)存域中。
拿以下的代碼作為例子溉愁,
synchonized(LockObject) {
//do something here
}
在對(duì)象LockObject的對(duì)象頭中处铛,當(dāng)其被創(chuàng)建的時(shí)候,其Markword的結(jié)構(gòu)如下:
bit fields | 是否偏向鎖 | 鎖標(biāo)志位 | |
---|---|---|---|
hash | age | 0 | 01 |
從上面Markword的結(jié)構(gòu)中,可以看出
所有新創(chuàng)建的對(duì)象撤蟆,都是可偏向的(鎖標(biāo)志位為01)奕塑,但都是未偏向的(是否偏向鎖標(biāo)志位為0)。
偏向鎖
當(dāng)線程執(zhí)行到臨界區(qū)(critical section)時(shí)家肯,此時(shí)會(huì)利用CAS(Compare and Swap)操作龄砰,將線程ID插入到Markword中,同時(shí)修改偏向鎖的標(biāo)志位讨衣。
這說(shuō)明此對(duì)象就要被當(dāng)做一個(gè)鎖來(lái)使用换棚,那么其Markword的內(nèi)容就要發(fā)生變化了。
其結(jié)構(gòu)其會(huì)變成如下:
bit fields | 是否偏向鎖 | 鎖標(biāo)志位 | ||
---|---|---|---|---|
threadId | epoch | age | 1 | 01 |
可以看到反镇,
- 鎖的標(biāo)志位還是01
- “是否偏向鎖”這個(gè)字段變成了1
- hash值變成了線程ID和epoch值
也就是說(shuō)固蚤,這個(gè)鎖將自己偏向了當(dāng)前線程,心里默默地藏著線程id愿险, 在這里颇蜡,我們就引入了“偏向鎖”的概念。
在此線程之后的執(zhí)行過(guò)程中辆亏,如果再次進(jìn)入或者退出同一段同步塊代碼风秤,并不再需要去進(jìn)行加鎖或者解鎖操作,而是會(huì)做以下的步驟:
- Load-and-test扮叨,也就是簡(jiǎn)單判斷一下當(dāng)前線程id是否與Markword當(dāng)中的線程id是否一致.
- 如果一致缤弦,則說(shuō)明此線程已經(jīng)成功獲得了鎖,繼續(xù)執(zhí)行下面的代碼
- 如果不一致彻磁,則要檢查一下對(duì)象是否還是可偏向碍沐,即“是否偏向鎖”標(biāo)志位的值。
- 如果還未偏向衷蜓,則利用CAS操作來(lái)競(jìng)爭(zhēng)鎖累提,也即是第一次獲取鎖時(shí)的操作。
- 如果此對(duì)象已經(jīng)偏向了磁浇,并且不是偏向自己斋陪,則說(shuō)明存在了競(jìng)爭(zhēng)。此時(shí)可能就要根據(jù)另外線程的情況置吓,可能是重新偏向无虚,也有可能是做偏向撤銷,但大部分情況下就是升級(jí)成輕量級(jí)鎖了衍锚。
以下是Java開(kāi)發(fā)人員提供的一張圖:
“偏向鎖”是Java在1.6引入的一種優(yōu)化機(jī)制友题,其核心思想在于,可以讓同一個(gè)線程一直擁有同一個(gè)鎖戴质,直到出現(xiàn)競(jìng)爭(zhēng)度宦,才去釋放鎖踢匣。
因?yàn)榻?jīng)過(guò)虛擬機(jī)開(kāi)發(fā)人員的調(diào)查研究,在大多數(shù)情況下斗埂,總是同一個(gè)線程去訪問(wèn)同步塊代碼符糊,基于這樣一個(gè)假設(shè),引入了偏向鎖呛凶,只需要用一個(gè)CAS操作和簡(jiǎn)單地判斷比較男娄,就可以讓一個(gè)線程持續(xù)地?fù)碛幸粋€(gè)鎖。
也正因?yàn)榇思僭O(shè)漾稀,在Jdk1.6中模闲,偏向鎖的開(kāi)關(guān)是默認(rèn)開(kāi)啟的,適用于只有一個(gè)線程訪問(wèn)同步塊的場(chǎng)景崭捍。
鎖膨脹
在上面尸折,我們講到,一旦出現(xiàn)競(jìng)爭(zhēng)殷蛇,也即有另外一個(gè)線程也要來(lái)訪問(wèn)這一段代碼实夹,偏向鎖就不適用于這種場(chǎng)景了。
如果兩個(gè)線程都是活躍的粒梦,會(huì)發(fā)生競(jìng)爭(zhēng)亮航,此時(shí)偏向鎖就會(huì)發(fā)生升級(jí),也就是我們常常聽(tīng)到的鎖膨脹匀们。
偏向鎖會(huì)膨脹成輕量級(jí)鎖(lightweight locking)缴淋。
鎖撤銷
偏向鎖有一個(gè)不好的點(diǎn)就是,一旦出現(xiàn)多線程競(jìng)爭(zhēng)泄朴,需要升級(jí)成輕量級(jí)鎖重抖,是有可能需要先做出銷撤銷的操作。
而銷撤銷的操作祖灰,相對(duì)來(lái)說(shuō)钟沛,開(kāi)銷就會(huì)比較大,其步驟如下:
- 在一個(gè)安全點(diǎn)停止擁有鎖的線程局扶,就跟開(kāi)始做GC操作一樣恨统。
- 遍歷線程棧,如果存在鎖記錄的話详民,需要修復(fù)鎖記錄和Markword延欠,使其變成無(wú)鎖狀態(tài)陌兑。
- 喚醒當(dāng)前線程沈跨,將當(dāng)前鎖升級(jí)成輕量級(jí)鎖。
輕量級(jí)鎖
而本質(zhì)上呢兔综,其實(shí)就是鎖對(duì)象頭中的Markword內(nèi)容又要發(fā)生變化了饿凛。
下面先簡(jiǎn)單地描述 其膨脹的步驟:
- 線程在自己的棧楨中創(chuàng)建鎖記錄 LockRecord
- 將鎖對(duì)象的對(duì)象頭中的MarkWord復(fù)制到線程的剛剛創(chuàng)建的鎖記錄中
- 將鎖記錄中的Owner指針指向鎖對(duì)象
- 將鎖對(duì)象的對(duì)象頭的MarkWord替換為指向鎖記錄的指針狞玛。
同樣,我們還是利用Java開(kāi)發(fā)人員提供的一張圖來(lái)描述此步驟:
可以根據(jù)上面兩圖來(lái)印證上面幾個(gè)步驟涧窒,但在這里心肪,其實(shí)對(duì)象的Markword其實(shí)也是發(fā)生了變化的,其現(xiàn)在的內(nèi)容結(jié)構(gòu)如下:
bit fields | 鎖標(biāo)志位 |
---|---|
指向LockRecord的指針 | 00 |
說(shuō)到這里纠吴,我們又通過(guò)偏向鎖引入了輕量級(jí)鎖的概念硬鞍,那么輕量級(jí)鎖是怎么個(gè)輕量級(jí)法,它具體的實(shí)現(xiàn)又是怎么樣的呢戴已?
就像偏向鎖的前提固该,是同步代碼塊在大多數(shù)情況下只有同一個(gè)線程訪問(wèn)的時(shí)候。
而輕量級(jí)鎖的前提則是糖儡,線程在同步代碼塊里面的操作非撤セ担快,獲取鎖之后握联,很快就結(jié)束操作桦沉,然后將鎖釋放出來(lái)。
但是不管再怎么快金闽,一旦一個(gè)線程獲得鎖了纯露,那么另一個(gè)線程同時(shí)也來(lái)訪問(wèn)這段代碼時(shí),怎么辦呢呐矾?這就涉及到我們下面所說(shuō)的鎖自旋的概念了苔埋。
自旋鎖/自適應(yīng)自旋鎖
來(lái)到輕量級(jí)鎖,其實(shí)輕量級(jí)的敘述就來(lái)自于自旋的概念蜒犯。
因?yàn)榍疤崾蔷€程在臨界區(qū)的操作非匙殚希快,所以它會(huì)非撤K妫快速地釋放鎖玉工,所以只要讓另外一個(gè)線程在那里地循環(huán)等待,然后當(dāng)鎖被釋放時(shí)淘菩,它馬上就能夠獲得鎖遵班,然后進(jìn)入臨界區(qū)執(zhí)行,然后馬上又釋放鎖潮改,讓給另外一個(gè)線程狭郑。
所謂自旋,就是線程在原地空循環(huán)地等待汇在,不阻塞翰萨,但它是消耗CPU的。
所以對(duì)于輕量級(jí)鎖糕殉,它也有其限制所在:
-
因?yàn)橄腃PU亩鬼,所以自旋的次數(shù)是有限的殖告,如果自旋到達(dá)一定的次數(shù)之后,還獲取不到鎖雳锋,那這種自旋也就無(wú)意義黄绩。但在上述的前提下,這種自旋的次數(shù)還是比較少的(經(jīng)驗(yàn)數(shù)據(jù))玷过。
當(dāng)然爽丹,一開(kāi)始的自旋次數(shù)都是固定的,但是在經(jīng)驗(yàn)代碼中辛蚊,獲得鎖的線程通常能夠馬上再獲得鎖习劫,所以又引入了自適應(yīng)的自旋,即根據(jù)上次獲得鎖的情況和當(dāng)前的線程狀態(tài)嚼隘,動(dòng)態(tài)地修改當(dāng)前線程自旋的次數(shù)诽里。
當(dāng)另一個(gè)線程釋放鎖之后,當(dāng)前線程要能夠馬上獲得鎖飞蛹,所以如果有超過(guò)兩個(gè)的線程同時(shí)訪問(wèn)這段代碼谤狡,就算另外一個(gè)線程釋放鎖之后,當(dāng)前線程也可能獲取不到鎖卧檐,還是要繼續(xù)等待墓懂,空耗CPU。
從以上兩點(diǎn)可以看出霉囚,當(dāng)線程通過(guò)自旋獲取不到鎖了捕仔,比如臨界區(qū)的操作太花時(shí)間了,或者有超過(guò)2個(gè)以上的線程在競(jìng)爭(zhēng)鎖了盈罐,輕量級(jí)鎖的前提又不成立了榜跌。當(dāng)虛擬機(jī)檢查到這種情況時(shí),又開(kāi)始了膨脹的腳步盅粪。
互斥鎖(重量級(jí)鎖)
相比起輕量級(jí)鎖钓葫,再膨脹的鎖,一般稱之為重量級(jí)鎖票顾,因?yàn)槭且蕾囉诿總€(gè)對(duì)象內(nèi)部都有的monitor鎖來(lái)實(shí)現(xiàn)的础浮,而monitor又依賴于操作系統(tǒng)的MutexLock(互斥鎖)來(lái)實(shí)現(xiàn),所以一般重量級(jí)鎖也叫互斥鎖奠骄。
由于需要在操作系統(tǒng)的內(nèi)核態(tài)和用戶態(tài)之間切換的豆同,需要將線程阻塞掛起,切換線程的上下文含鳞,再恢復(fù)等操作影锈,所以當(dāng)synchronized升級(jí)成互斥鎖,依賴monitor的時(shí)候,開(kāi)銷就比較大了精居,而這也是之前為什么說(shuō)synchronized是一個(gè)很重的操作的原因了。
當(dāng)然潜必,升級(jí)成互斥鎖之后靴姿,鎖對(duì)象頭的Markword內(nèi)容也是會(huì)變化的,其內(nèi)容如下:
bit fields | 鎖標(biāo)志位 |
---|---|
指向Mutex的指針 | 10 |
每次檢查當(dāng)前線程是否獲得鎖磁滚,其實(shí)就是檢查Mutex的值是否為0佛吓,不為0,說(shuō)明其為其線程所占有垂攘,此時(shí)操作系統(tǒng)就會(huì)介入维雇,將線程阻塞,掛起晒他,釋放CPU時(shí)間吱型,等待下一次的線程調(diào)度。
好了陨仅,到這里津滞,對(duì)于synchronized所修改的同步方法或者同步代碼塊,虛擬機(jī)是如何操作的灼伤,大家應(yīng)該也有一個(gè)簡(jiǎn)單的印象了触徐。
當(dāng)使用synchronized關(guān)鍵字的時(shí)候,在java1.6之后狐赡,根據(jù)不同的條件和場(chǎng)景撞鹉,虛擬機(jī)是一步一步地將偏向鎖升級(jí)成輕量級(jí)鎖,再最終升級(jí)成重量級(jí)鎖的颖侄,而這個(gè)過(guò)程是不可逆的鸟雏,因?yàn)橐坏┥?jí)成重量級(jí)鎖,則說(shuō)明偏向鎖和輕量級(jí)鎖是不適用于當(dāng)前的應(yīng)用場(chǎng)景的览祖,那再降級(jí)回去也沒(méi)什么意義崔慧。
從這一點(diǎn),也可以看出穴墅,如果我們的應(yīng)用場(chǎng)景本身就不適用于偏向鎖和輕量級(jí)鎖惶室,那么我們?cè)诔绦蛞婚_(kāi)始,就應(yīng)該禁用掉偏向鎖和輕量級(jí)鎖玄货,直接使用重量級(jí)鎖皇钞,省去無(wú)謂的開(kāi)銷。
總結(jié)
在這里總結(jié)一下松捉,在使用synchronized關(guān)鍵字的時(shí)候夹界,本質(zhì)上是否獲得鎖,是通過(guò)修改鎖對(duì)象頭中的markword的內(nèi)容來(lái)標(biāo)記是否獲得鎖隘世,并由虛擬機(jī)來(lái)根據(jù)具體的應(yīng)用場(chǎng)景來(lái)鎖進(jìn)行升級(jí)可柿。
簡(jiǎn)單地將上述幾個(gè)零散的markword變化合在一起鸠踪,展示在下面:
鎖狀態(tài) | bits | 1bit是否是偏向鎖 | 2bit鎖標(biāo)志位 |
---|---|---|---|
無(wú)鎖狀態(tài) | 對(duì)象的hashCode | 0 | 01 |
偏向鎖 | 線程ID | 1 | 01 |
輕量級(jí)鎖 | 指向棧中鎖記錄的指針 | 0 | 00 |
重量級(jí)鎖 | 指向互斥量的指針 | 0 | 10 |
【參考資料】
Eliminating Synchronization Related Atomic Operations with Biased Locking and Bulk Rebiasing