徹底搞懂synchronized(從偏向鎖到重量級(jí)鎖)
接觸過線程安全的同學(xué)想必都使用過synchronized這個(gè)關(guān)鍵字溃卡,在java同步代碼快中铐维,synchronized的使用方式無非有兩個(gè):
- 通過對(duì)一個(gè)對(duì)象進(jìn)行加鎖來實(shí)現(xiàn)同步焰坪,如下面代碼。
synchronized(lockObject){ //代碼 }
- 對(duì)一個(gè)方法進(jìn)行synchronized聲明锥余,進(jìn)而對(duì)一個(gè)方法進(jìn)行加鎖來實(shí)現(xiàn)同步瓮恭。如下面代碼
public synchornized void test(){ //代碼}
但這里需要指出的是,無論是對(duì)一個(gè)對(duì)象進(jìn)行加鎖還是對(duì)一個(gè)方法進(jìn)行加鎖产还,實(shí)際上匹厘,都是對(duì)對(duì)象進(jìn)行加鎖。
也就是說脐区,對(duì)于方式2愈诚,實(shí)際上虛擬機(jī)會(huì)根據(jù)synchronized修飾的是實(shí)例方法還是類方法,去取對(duì)應(yīng)的實(shí)例對(duì)象或者Class對(duì)象來進(jìn)行加鎖牛隅。
對(duì)于synchronized這個(gè)關(guān)鍵字炕柔,可能之前大家有聽過,他是一個(gè)重量級(jí)鎖媒佣,開銷很大匕累,建議大家少用點(diǎn)。但大家可能也聽說過默伍,但到了jdk1.6之后欢嘿,該關(guān)鍵字被進(jìn)行了很多的優(yōu)化,已經(jīng)不像以前那樣不給力了也糊,建議大家多使用括堤。
那么它是進(jìn)行了什么樣的優(yōu)化速侈,才使得synchronized又深得人心呢?為何重量級(jí)鎖開銷就大呢涩盾?
想必大家也都聽說過輕量級(jí)鎖辛辨,重量級(jí)鎖捕捂,自旋鎖,自適應(yīng)自旋鎖斗搞,偏向鎖等等指攒,他們都有哪些區(qū)別呢?
剛才和大家說僻焚,鎖是加在對(duì)象上的允悦,那么一個(gè)線程是如何知道這個(gè)對(duì)象被加了鎖呢?又是如何知道它加的是什么類型的鎖呢虑啤?
基于這些問題隙弛,下面我講一步一步講解synchronized是如何被優(yōu)化的架馋,是如何從偏向鎖到重量級(jí)鎖的。
鎖對(duì)象
剛才我們說全闷,鎖實(shí)際上是加在對(duì)象上的叉寂,那么被加了鎖的對(duì)象我們稱之為鎖對(duì)象,在java中总珠,任何一個(gè)對(duì)象都能成為鎖對(duì)象屏鳍。
為了讓大家更好著理解虛擬機(jī)是如何知道這個(gè)對(duì)象就是一個(gè)鎖對(duì)象的,我們下面簡單介紹一下java中一個(gè)對(duì)象的結(jié)構(gòu)局服。
java對(duì)象在內(nèi)存中的存儲(chǔ)結(jié)構(gòu)主要有一下三個(gè)部分:
- 對(duì)象頭
- 實(shí)例數(shù)據(jù)
- 填充數(shù)據(jù)
這里強(qiáng)調(diào)一下钓瞭,對(duì)象頭里的數(shù)據(jù)主要是一些運(yùn)行時(shí)的數(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)對(duì)象為數(shù)組時(shí)) |
從該表格中我們可以看到山涡,對(duì)象中關(guān)于鎖的信息是存在Markword里的。
我們來看一段代碼
LockObject lockObject = new LockObject();//隨便創(chuàng)建一個(gè)對(duì)象synchronized(lockObject){ //代碼}
當(dāng)我們創(chuàng)建一個(gè)對(duì)象LockObject時(shí)搏讶,該對(duì)象的部分Markword關(guān)鍵數(shù)據(jù)如下佳鳖。
bit fields | 是否偏向鎖 | 鎖標(biāo)志位 |
---|---|---|
hash | 0 | 01 |
從圖中可以看出,偏向鎖的標(biāo)志位是“01”媒惕,狀態(tài)是“0”系吩,表示該對(duì)象還沒有被加上偏向鎖。(“1”是表示被加上偏向鎖)妒蔚。該對(duì)象被創(chuàng)建出來的那一刻穿挨,就有了偏向鎖的標(biāo)志位,這也說明了所有對(duì)象都是可偏向的肴盏,但所有對(duì)象的狀態(tài)都為“0”科盛,也同時(shí)說明所有被創(chuàng)建的對(duì)象的偏向鎖并沒有生效。
偏向鎖
不過菜皂,當(dāng)線程執(zhí)行到臨界區(qū)(critical section)時(shí)贞绵,此時(shí)會(huì)利用CAS(Compare and Swap)操作,將線程ID插入到Markword中恍飘,同時(shí)修改偏向鎖的標(biāo)志位榨崩。
所謂臨界區(qū),就是只允許一個(gè)線程進(jìn)去執(zhí)行操作的區(qū)域章母,即同步代碼塊母蛛。CAS是一個(gè)原子性操作
此時(shí)的Mark word的結(jié)構(gòu)信息如下:
bit fields | 是否偏向鎖 | 鎖標(biāo)志位 | |
---|---|---|---|
threadId | epoch | 1 | 01 |
此時(shí)偏向鎖的狀態(tài)為“1”,說明對(duì)象的偏向鎖生效了乳怎,同時(shí)也可以看到彩郊,哪個(gè)線程獲得了該對(duì)象的鎖。
那么,什么是偏向鎖?
偏向鎖是jdk1.6引入的一項(xiàng)鎖優(yōu)化秫逝,其中的“偏”是偏心的偏恕出。它的意思就是說,這個(gè)鎖會(huì)偏向于第一個(gè)獲得它的線程筷登,在接下來的執(zhí)行過程中剃根,假如該鎖沒有被其他線程所獲取,沒有其他線程來競爭該鎖前方,那么持有偏向鎖的線程將永遠(yuǎn)不需要進(jìn)行同步操作狈醉。
也就是說:
在此線程之后的執(zhí)行過程中,如果再次進(jìn)入或者退出同一段同步塊代碼惠险,并不再需要去進(jìn)行加鎖或者解鎖操作苗傅,而是會(huì)做以下的步驟:
- Load-and-test,也就是簡單判斷一下當(dāng)前線程id是否與Markword當(dāng)中的線程id是否一致.
- 如果一致班巩,則說明此線程已經(jīng)成功獲得了鎖渣慕,繼續(xù)執(zhí)行下面的代碼.
- 如果不一致,則要檢查一下對(duì)象是否還是可偏向抱慌,即“是否偏向鎖”標(biāo)志位的值逊桦。
- 如果還未偏向,則利用CAS操作來競爭鎖抑进,也即是第一次獲取鎖時(shí)的操作强经。
如果此對(duì)象已經(jīng)偏向了,并且不是偏向自己寺渗,則說明存在了競爭匿情。此時(shí)可能就要根據(jù)另外線程的情況,可能是重新偏向信殊,也有可能是做偏向撤銷炬称,但大部分情況下就是升級(jí)成輕量級(jí)鎖了。
可以看出涡拘,偏向鎖是針對(duì)于一個(gè)線程而言的玲躯,線程獲得鎖之后就不會(huì)再有解鎖等操作了,這樣可以省略很多開銷鳄乏。假如有兩個(gè)線程來競爭該鎖話府蔗,那么偏向鎖就失效了,進(jìn)而升級(jí)成輕量級(jí)鎖了汞窗。
為什么要這樣做呢?因?yàn)榻?jīng)驗(yàn)表明赡译,其實(shí)大部分情況下仲吏,都會(huì)是同一個(gè)線程進(jìn)入同一塊同步代碼塊的。這也是為什么會(huì)有偏向鎖出現(xiàn)的原因。
在Jdk1.6中裹唆,偏向鎖的開關(guān)是默認(rèn)開啟的誓斥,適用于只有一個(gè)線程訪問同步塊的場景。
鎖膨脹
剛才說了许帐,當(dāng)出現(xiàn)有兩個(gè)線程來競爭鎖的話劳坑,那么偏向鎖就失效了,此時(shí)鎖就會(huì)膨脹成畦,升級(jí)為輕量級(jí)鎖距芬。這也是我們經(jīng)常所說的鎖膨脹
鎖撤銷
由于偏向鎖失效了,那么接下來就得把該鎖撤銷循帐,鎖撤銷的開銷花費(fèi)還是挺大的框仔,其大概的過程如下:
- 在一個(gè)安全點(diǎn)停止擁有鎖的線程。
- 遍歷線程棧拄养,如果存在鎖記錄的話离斩,需要修復(fù)鎖記錄和Markword,使其變成無鎖狀態(tài)瘪匿。
- 喚醒當(dāng)前線程跛梗,將當(dāng)前鎖升級(jí)成輕量級(jí)鎖。
所以棋弥,如果某些同步代碼塊大多數(shù)情況下都是有兩個(gè)及以上的線程競爭的話核偿,那么偏向鎖就會(huì)是一種累贅,對(duì)于這種情況嘁锯,我們可以一開始就把偏向鎖這個(gè)默認(rèn)功能給關(guān)閉
輕量級(jí)鎖
鎖撤銷升級(jí)為輕量級(jí)鎖之后宪祥,那么對(duì)象的Markword也會(huì)進(jìn)行相應(yīng)的的變化。下面先簡單描述下鎖撤銷之后家乘,升級(jí)為輕量級(jí)鎖的過程:
- 線程在自己的棧楨中創(chuàng)建鎖記錄 LockRecord蝗羊。
- 將鎖對(duì)象的對(duì)象頭中的MarkWord復(fù)制到線程的剛剛創(chuàng)建的鎖記錄中。
- 將鎖記錄中的Owner指針指向鎖對(duì)象仁锯。
- 將鎖對(duì)象的對(duì)象頭的MarkWord替換為指向鎖記錄的指針耀找。
對(duì)應(yīng)的圖描述如下(圖來自周志明深入java虛擬機(jī))
之后Markwork如下:
bit fields | 鎖標(biāo)志位 |
---|---|
指向LockRecord的指針 | 00 |
注:鎖標(biāo)志位”00”表示輕量級(jí)鎖
輕量級(jí)鎖主要有兩種
- 自旋鎖
- 自適應(yīng)自旋鎖
自旋鎖
所謂自旋,就是指當(dāng)有另外一個(gè)線程來競爭鎖時(shí)业崖,這個(gè)線程會(huì)在原地循環(huán)等待野芒,而不是把該線程給阻塞,直到那個(gè)獲得鎖的線程釋放鎖之后双炕,這個(gè)線程就可以馬上獲得鎖的狞悲。
注意,鎖在原地循環(huán)的時(shí)候妇斤,是會(huì)消耗cpu的摇锋,就相當(dāng)于在執(zhí)行一個(gè)啥也沒有的for循環(huán)丹拯。
所以,輕量級(jí)鎖適用于那些同步代碼塊執(zhí)行的很快的場景荸恕,這樣乖酬,線程原地等待很短很短的時(shí)間就能夠獲得鎖了。
經(jīng)驗(yàn)表明融求,大部分同步代碼塊執(zhí)行的時(shí)間都是很短很短的咬像,也正是基于這個(gè)原因,才有了輕量級(jí)鎖這么個(gè)東西生宛。
自旋鎖的一些問題
- 如果同步代碼塊執(zhí)行的很慢县昂,需要消耗大量的時(shí)間,那么這個(gè)時(shí)侯茅糜,其他線程在原地等待空消耗cpu七芭,這會(huì)讓人很難受。
- 本來一個(gè)線程把鎖釋放之后蔑赘,當(dāng)前線程是能夠獲得鎖的狸驳,但是假如這個(gè)時(shí)候有好幾個(gè)線程都在競爭這個(gè)鎖的話,那么有可能當(dāng)前線程會(huì)獲取不到鎖缩赛,還得原地等待繼續(xù)空循環(huán)消耗cup耙箍,甚至有可能一直獲取不到鎖。
基于這個(gè)問題酥馍,我們必須給線程空循環(huán)設(shè)置一個(gè)次數(shù)辩昆,當(dāng)線程超過了這個(gè)次數(shù),我們就認(rèn)為旨袒,繼續(xù)使用自旋鎖就不適合了汁针,此時(shí)鎖會(huì)再次膨脹,升級(jí)為重量級(jí)鎖砚尽。
默認(rèn)情況下施无,自旋的次數(shù)為10次,用戶可以通過-XX:PreBlockSpin來進(jìn)行更改必孤。
自旋鎖是在JDK1.4.2的時(shí)候引入的
自適應(yīng)自旋鎖
所謂自適應(yīng)自旋鎖就是線程空循環(huán)等待的自旋次數(shù)并非是固定的猾骡,而是會(huì)動(dòng)態(tài)著根據(jù)實(shí)際情況來改變自旋等待的次數(shù)。
其大概原理是這樣的:
假如一個(gè)線程1剛剛成功獲得一個(gè)鎖敷搪,當(dāng)它把鎖釋放了之后兴想,線程2獲得該鎖,并且線程2在運(yùn)行的過程中赡勘,此時(shí)線程1又想來獲得該鎖了嫂便,但線程2還沒有釋放該鎖,所以線程1只能自旋等待闸与,但是虛擬機(jī)認(rèn)為顽悼,由于線程1剛剛獲得過該鎖曼振,那么虛擬機(jī)覺得線程1這次自旋也是很有可能能夠再次成功獲得該鎖的,所以會(huì)延長線程1自旋的次數(shù)蔚龙。
另外,如果對(duì)于某一個(gè)鎖映胁,一個(gè)線程自旋之后木羹,很少成功獲得該鎖,那么以后這個(gè)線程要獲取該鎖時(shí)解孙,是有可能直接忽略掉自旋過程坑填,直接升級(jí)為重量級(jí)鎖的,以免空循環(huán)等待浪費(fèi)資源弛姜。
輕量級(jí)鎖也被稱為非阻塞同步脐瑰、樂觀鎖,因?yàn)檫@個(gè)過程并沒有把線程阻塞掛起廷臼,而是讓線程空循環(huán)等待苍在,串行執(zhí)行。
重量級(jí)鎖
輕量級(jí)鎖膨脹之后荠商,就升級(jí)為重量級(jí)鎖了寂恬。重量級(jí)鎖是依賴對(duì)象內(nèi)部的monitor鎖來實(shí)現(xiàn)的,而monitor又依賴操作系統(tǒng)的MutexLock(互斥鎖)來實(shí)現(xiàn)的莱没,所以重量級(jí)鎖也被成為互斥鎖初肉。
當(dāng)輕量級(jí)所經(jīng)過鎖撤銷等步驟升級(jí)為重量級(jí)鎖之后,它的Markword部分?jǐn)?shù)據(jù)大體如下
bit fields | 鎖標(biāo)志位 |
---|---|
指向Mutex的指針 | 10 |
為什么說重量級(jí)鎖開銷大呢
主要是饰躲,當(dāng)系統(tǒng)檢查到鎖是重量級(jí)鎖之后牙咏,會(huì)把等待想要獲得鎖的線程進(jìn)行阻塞,被阻塞的線程不會(huì)消耗cup嘹裂。但是阻塞或者喚醒一個(gè)線程時(shí)妄壶,都需要操作系統(tǒng)來幫忙,這就需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài)焦蘑,而轉(zhuǎn)換狀態(tài)是需要消耗很多時(shí)間的盯拱,有可能比用戶執(zhí)行代碼的時(shí)間還要長。
這就是說為什么重量級(jí)線程開銷很大的例嘱。
互斥鎖(重量級(jí)鎖)也稱為阻塞同步狡逢、悲觀鎖
總結(jié)
通過上面的分析,我們知道了為什么synchronized關(guān)鍵字為何又深得人心拼卵,也知道了鎖的演變過程奢浑。
也就是說,synchronized關(guān)鍵字并非一開始就該對(duì)象加上重量級(jí)鎖腋腮,也是從偏向鎖雀彼,輕量級(jí)鎖壤蚜,再到重量級(jí)鎖的過程。
這個(gè)過程也告訴我們徊哑,假如我們一開始就知道某個(gè)同步代碼塊的競爭很激烈袜刷、很慢的話,那么我們一開始就應(yīng)該使用重量級(jí)鎖了莺丑,從而省掉一些鎖轉(zhuǎn)換的開銷著蟹。
講到這里就大概完了,希望能對(duì)你有所幫助
完
參考資料
- 深入理解java虛擬機(jī)(JVM高級(jí)特性與最佳實(shí)踐)
- java并發(fā)編程
- Eliminating Synchronization Related Atomic Operations with Biased Locking and Bulk Rebiasing