synchronized瞪慧,在java并發(fā)編程中它一直都是元老級(jí)的角色鸟廓。但是在大多數(shù)時(shí)候影暴,如果能使用Lock大家可能都不會(huì)使用它括改,因?yàn)樗莻€(gè)重量級(jí)鎖腻豌。但是隨著jdk6引入偏向鎖和輕量級(jí)鎖,對(duì)它進(jìn)行了各種優(yōu)化之后嘱能,在一些情況下它并不是那么重了饲梭。本文將結(jié)合HotSpot 1.7源碼,詳細(xì)分析jdk6做出的相關(guān)優(yōu)化焰檩。
synchronized實(shí)現(xiàn)分析
在開(kāi)始分析synchronized具體實(shí)現(xiàn)之前憔涉,先了解一下java同步的基礎(chǔ)。
Java同步基礎(chǔ)
在java中析苫,每一個(gè)對(duì)象其實(shí)都可以作為鎖:
對(duì)于同步方法兜叨,鎖就是當(dāng)前的實(shí)例對(duì)象;
對(duì)于靜態(tài)同步方法衩侥,鎖就是當(dāng)前對(duì)象的Class對(duì)象国旷;
對(duì)于同步方法塊,鎖是synchronized括號(hào)里的對(duì)象茫死。
當(dāng)一個(gè)線程嘗試去訪問(wèn)同步代碼塊兒時(shí)跪但,首先需要干的事兒就是得到鎖,然后在程序執(zhí)行完畢或者拋出異常時(shí)釋放鎖峦萎,那么現(xiàn)在問(wèn)題就來(lái)了屡久,鎖存放在哪里呢?鎖需要存儲(chǔ)什么信息呢爱榔?
synchronized字節(jié)碼分析
javap命令反編譯后的字節(jié)碼:
從上圖可以看出被环,字節(jié)碼中包含指令monitorenter和moniterexit。synchronized關(guān)鍵字基于這兩個(gè)指令實(shí)現(xiàn)了代碼同步塊鎖的獲取和釋放详幽。
注:在JVM規(guī)范中筛欢,代碼塊同步是使用指令monitorenter和moniterexit實(shí)現(xiàn)浸锨,而方法同步是使用另外一種方式實(shí)現(xiàn),具體的實(shí)現(xiàn)細(xì)節(jié)JVM規(guī)范沒(méi)有做詳細(xì)說(shuō)明版姑。monitorenter指令是在編譯后插入到同步代碼塊的開(kāi)始位置柱搜,moniterexit指令是插入到同步代碼快結(jié)束和異常出,JVM要保證每個(gè)monitorenter指令都必須有moniterexit指令與之配對(duì)剥险。JVM中的任何對(duì)象都有一個(gè)monitor與之關(guān)聯(lián)冯凹,當(dāng)有一個(gè)monitor被持有后,它將處于鎖定狀態(tài)炒嘲,而線程執(zhí)行到monitorenter指令時(shí)宇姚,將會(huì)嘗試獲取對(duì)象對(duì)應(yīng)的monitor的所有權(quán),這個(gè)過(guò)程也就是所謂的嘗試獲取對(duì)象的鎖夫凸。
monitorenter實(shí)現(xiàn)
整個(gè)monitorenter主要干了這些事兒:
將入?yún)avaThread thread指向當(dāng)前線程浑劳;
初始化當(dāng)前線程的對(duì)象頭;
判斷當(dāng)前虛擬機(jī)是否開(kāi)啟偏向鎖功能夭拌,如果開(kāi)啟魔熏,調(diào)用fast_enter方法,否則鸽扁,調(diào)用slow_enter方法蒜绽。
Java對(duì)象頭
monitorenter中很重要的一步就是構(gòu)造Java對(duì)象頭h_obj,同時(shí)桶现,在后續(xù)的fast_enter或者slow_enter中躲雅,h_obj都作為一個(gè)入?yún)⑴c到具體的邏輯中,鎖其實(shí)就存儲(chǔ)在Java對(duì)象頭中骡和。
對(duì)象頭組成部分
如果對(duì)象類型是數(shù)組相赁,虛擬機(jī)用3個(gè)Word存儲(chǔ)對(duì)象頭,如果對(duì)象類型是非數(shù)組類型慰于,用2個(gè)Word存儲(chǔ)對(duì)象頭钮科,接下來(lái)看看這幾個(gè)Word都用來(lái)干什么。
Mark Word:主要用來(lái)存儲(chǔ)對(duì)象的hashCode婆赠、鎖標(biāo)記位绵脯、分代年齡等等,占用內(nèi)存大小為1個(gè)Word休里;
Class Metadata Address:主要用來(lái)存儲(chǔ)對(duì)象類型數(shù)據(jù)的指針蛆挫,占用內(nèi)存大小為1個(gè)Word;
Array Length:存儲(chǔ)數(shù)組的長(zhǎng)度份帐,這部分只有在當(dāng)前對(duì)象類型為數(shù)組時(shí)才存在璃吧,同樣楣导,占用內(nèi)存大小也為1個(gè)Word废境。
接下來(lái)就詳細(xì)了解與synchronized息息相關(guān)的Mark Word的相關(guān)內(nèi)容。
HotSpot的Mark Word
HotSpot通過(guò)markOop.hpp實(shí)現(xiàn)了Mark Word。由于對(duì)象頭需要存儲(chǔ)的數(shù)據(jù)類型較多噩凹,充分考慮到內(nèi)存的復(fù)用巴元,markOop被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu),可以根據(jù)標(biāo)志位的變化而轉(zhuǎn)變成不同類型的數(shù)據(jù)驮宴。
-
32位虛擬機(jī)markOop實(shí)現(xiàn)
32位虛擬機(jī)markOop實(shí)現(xiàn).png- hash:對(duì)象hashCode逮刨;
- age:對(duì)象的分代年齡;
- biased_lock:是否是偏向鎖堵泽;
- lock:鎖標(biāo)志位修己;
- JavaThread*:持有偏向鎖的線程ID;
- epoch:偏向鎖時(shí)間戳
在運(yùn)行期間迎罗,隨著鎖標(biāo)志位的變化睬愤,Mark Word可以變化成以下幾種類型的數(shù)據(jù):
Mark Word數(shù)據(jù)類型 -
64虛擬機(jī)markOop實(shí)現(xiàn)
64位markOop實(shí)現(xiàn)
在32位虛擬的markOop基礎(chǔ)上增加了unused,同樣的纹安,在運(yùn)行期間尤辱,隨著標(biāo)志位的變化Mark Work也會(huì)隨之改變,在這里我就不做詳細(xì)贅述了厢岂。
鎖的升級(jí)
jdk 6為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗光督,引入了偏向鎖和輕量級(jí)鎖,換句話說(shuō)塔粒,在jdk 6及以后版本结借,鎖一共有四種狀態(tài):無(wú)鎖狀態(tài)、偏向鎖狀態(tài)卒茬、輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài)映跟,它會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí)。但是扬虚,鎖一旦升級(jí)之后就不能降級(jí)努隙,當(dāng)然,不能降級(jí)也是為了提高獲得鎖和釋放鎖的效率辜昵。
偏向鎖
引入偏向鎖是為了讓線程獲取鎖的代價(jià)更低荸镊。當(dāng)一個(gè)線程訪問(wèn)同步代碼塊并且獲取鎖時(shí),會(huì)在對(duì)象頭和棧幀中的鎖記錄中存儲(chǔ)偏向鎖偏向的線程ID堪置,以后該線程在進(jìn)入和退出同步塊時(shí)不需要花費(fèi)CAS操作來(lái)加鎖和解鎖躬存,只需要校驗(yàn)對(duì)象頭Mark Work中是否存儲(chǔ)指向當(dāng)前線程的偏向鎖即可,節(jié)省了一部分CAS操作的性能消耗舀锨。不過(guò)岭洲,當(dāng)多個(gè)線程競(jìng)爭(zhēng)偏向鎖時(shí),需要撤銷偏向鎖坎匿,如果撤銷偏向鎖的性能消耗大于之前節(jié)省下來(lái)的那部分CAS操作的性能消耗盾剩,就得不償失了雷激。在jdk 6和jdk 7中,偏向鎖默認(rèn)是啟用的告私,但是它在應(yīng)用程序啟動(dòng)幾秒鐘之后才激活屎暇,當(dāng)然,如果不想偏向鎖延遲激活驻粟,可以使用JVM參數(shù)-XX:BiasedLockingStartupDelay = 0
來(lái)關(guān)閉延遲根悼。當(dāng)然,也可以用過(guò)JVM參數(shù)-XX:-UseBiasedLocking=false
來(lái)關(guān)閉偏向鎖蜀撑,這時(shí)候挤巡,默認(rèn)的鎖狀態(tài)是輕量級(jí)鎖。
在HosSpot中酷麦,偏向鎖的入口為synchronizer.cpp的fast_enter方法:
偏向鎖的獲取
注:偏向鎖獲取代碼過(guò)長(zhǎng)玄柏,在這里就不貼代碼了,有興趣的可以去openjdk對(duì)照相應(yīng)的源碼看看贴铜。
偏向鎖的獲取的實(shí)現(xiàn)邏輯如下:
獲取對(duì)象頭Mark Word mark粪摘;
判斷對(duì)象頭mark是否為可偏向狀態(tài),也就是判斷mark的偏向鎖biased_lock是否為1绍坝,lock狀態(tài)是否為01徘意;
判斷對(duì)象頭mark中的JavaThread* thread:
null == thread || thread == Thread.current,跳轉(zhuǎn)到步驟4轩褐;
否則椎咧,跳轉(zhuǎn)到步驟5;
- 調(diào)用CAS指令設(shè)置mark中的JavaThread為當(dāng)前線程:
調(diào)用CAS成功把介,返回BIAS_REVOKED勤讽,鎖獲取成功,線程可以執(zhí)行同步代碼塊拗踢;
調(diào)用CAS失敗脚牍,跳轉(zhuǎn)到步驟5;
- 當(dāng)調(diào)用CAS失敗時(shí)巢墅,表明當(dāng)前存在多個(gè)線程競(jìng)爭(zhēng)鎖诸狭,當(dāng)達(dá)到safepoint時(shí),掛起已獲得偏向鎖的線程君纫,撤銷偏向鎖驯遇,并且調(diào)用slow_enter方法將當(dāng)前鎖升級(jí)為輕量級(jí)鎖,獲取到輕量級(jí)鎖之后蓄髓,喚醒被阻塞在safepoint的線程叉庐,線程繼續(xù)執(zhí)行同步代碼塊。
偏向鎖的撤銷
具體執(zhí)行流程如下:
校驗(yàn)當(dāng)前是否到達(dá)safepoint会喝;
暫停已獲取到偏向鎖的線程陡叠;
撤銷偏向鎖玩郊,恢復(fù)鎖標(biāo)志位為01(無(wú)鎖狀態(tài))或者00(輕量級(jí)鎖狀態(tài))。
輕量級(jí)鎖
注:輕量級(jí)鎖的引入在一定程度上減少了鎖的性能消耗匾竿,但是如果多個(gè)線程競(jìng)爭(zhēng)時(shí)瓦宜,輕量級(jí)鎖還是會(huì)膨脹成重量級(jí)鎖蔚万,所以岭妖,輕量級(jí)鎖以及偏向鎖的出現(xiàn)并不是想要替代重量級(jí)鎖。
輕量級(jí)鎖的獲取
在HotSpot中反璃,輕量級(jí)鎖的入口為synchronizer.cpp的slow_enter方法:
具體執(zhí)行流程如下:
獲取對(duì)象頭mark昵慌;
調(diào)用方法
is_neutral()
判斷當(dāng)前對(duì)象是否為無(wú)鎖狀態(tài)(mark的biased_lock為0,lock為01):
無(wú)鎖狀態(tài)淮蜈,跳轉(zhuǎn)到步驟5斋攀;
否則,跳轉(zhuǎn)到步驟4梧田;
調(diào)用
set_displaced_header
方法將對(duì)象頭mark復(fù)制到鎖記錄中淳蔼;調(diào)用CAS指令嘗試將對(duì)象頭mark替換為指向鎖記錄的指針,如果成功裁眯,當(dāng)前線程獲取到鎖鹉梨,可以執(zhí)行同步代碼塊,否則穿稳,跳轉(zhuǎn)到步驟5存皂;
如果對(duì)象頭mark處于加鎖狀態(tài),并且mark的鎖記錄指針指向當(dāng)前線程逢艘,當(dāng)前線程獲取到鎖旦袋,可以執(zhí)行同步代碼塊,否則它改,當(dāng)前存在多個(gè)線程競(jìng)爭(zhēng)疤孕,調(diào)用inflate方法膨脹成重量級(jí)鎖。
輕量級(jí)鎖的釋放
輕量級(jí)鎖的釋放是通過(guò)synchronizer.cpp的fast_exit完成的:
具體執(zhí)行流程如下:
- 校驗(yàn)當(dāng)前對(duì)象頭mark是否不處于偏向鎖狀態(tài):
處于偏向鎖狀態(tài)央拖,校驗(yàn)不通過(guò)胰柑,程序不往下執(zhí)行;
不處于偏向鎖狀態(tài)爬泥,校驗(yàn)通過(guò)柬讨,跳轉(zhuǎn)到步驟2;
獲取保存在BasicLock對(duì)象中的對(duì)象頭dhw袍啡;
嘗試使用CAS操作將dhw替換到當(dāng)前對(duì)象頭踩官,如果替換成功,表示沒(méi)有競(jìng)爭(zhēng)發(fā)生境输,輕量級(jí)鎖釋放成功蔗牡,否則颖系,當(dāng)前鎖存在競(jìng)爭(zhēng),調(diào)用inflate方法膨脹成重量級(jí)鎖辩越。
到這里為止嘁扼,就jdk 6對(duì)synchronized關(guān)鍵字做出的相關(guān)優(yōu)化分析就告一段落了,synchronized還有一部分有關(guān)重量級(jí)鎖的實(shí)現(xiàn)也會(huì)在后文做相應(yīng)的介紹分析黔攒。希望對(duì)大家就synchronized關(guān)鍵詞理解有所幫助趁啸。