工作中經(jīng)常遇到需要用鎖來控制并發(fā)的問題羊娃,java中提供一個(gè)鎖神器關(guān)鍵字-Synchronized。通過它可以來解決多線程問題。與Java中另一個(gè)Lock鎖相比接校,之前一直覺得Synchronized是重量級(jí)的鎖筹吐,很耗性能糖耸,真的是這樣嗎?
Synchronized的實(shí)現(xiàn)原理
synchronized可以保證方法或者代碼塊在運(yùn)行時(shí)丘薛,同一時(shí)刻只有一個(gè)方法可以進(jìn)入到臨界區(qū)嘉竟,同時(shí)它還可以保證共享變量的內(nèi)存可見性
同步的基礎(chǔ)
Java中每一個(gè)對(duì)象都可以作為鎖,這是synchronized實(shí)現(xiàn)同步的基礎(chǔ):
- 普通同步方法洋侨,鎖是當(dāng)前實(shí)例對(duì)象
- 靜態(tài)同步方法舍扰,鎖是當(dāng)前類的class對(duì)象
- 同步方法塊,鎖是括號(hào)里面的對(duì)象
當(dāng)一個(gè)線程訪問同步代碼塊時(shí)希坚,它首先是需要得到鎖才能執(zhí)行同步代碼边苹,當(dāng)退出或者拋出異常時(shí)必須要釋放鎖,那么它是如何來實(shí)現(xiàn)這個(gè)機(jī)制的呢裁僧?我們先看一段簡單的代碼:
public class SynchronizedTest {
public synchronized void test1() {
//do something
}
public void test2() {
synchronized (this) {
//do something
}
}
同步的原理
???JVM規(guī)范規(guī)定JVM基于進(jìn)入和退出Monitor對(duì)象來實(shí)現(xiàn)方法同步和代碼塊同步个束,但兩者的實(shí)現(xiàn)細(xì)節(jié)不一樣。代碼塊同步是使用monitorenter和monitorexit指令實(shí)現(xiàn)聊疲,而方法同步是使用另外一種方式實(shí)現(xiàn)的茬底,細(xì)節(jié)在JVM規(guī)范里并沒有詳細(xì)說明,而是在Class文件的方法表中將該方法的access_flags字段中的synchronized標(biāo)志位置1获洲,表示該方法是同步方法并使用調(diào)用該方法的對(duì)象或該方法所屬的Class在JVM的內(nèi)部對(duì)象表示Class做為鎖對(duì)象阱表。
???monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而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ì)象的鎖蒜鸡。
???在虛擬機(jī)規(guī)范的要求,在執(zhí)行monitorenter指令時(shí)牢裳,首先嘗試獲取對(duì)象的鎖逢防,如果這個(gè)對(duì)象沒有被鎖,或者當(dāng)前線程已經(jīng)擁有這個(gè)對(duì)象的鎖蒲讯,把鎖的計(jì)數(shù)器加1忘朝,相應(yīng)的,在執(zhí)行monitorexit指令時(shí)會(huì)將計(jì)數(shù)器減1判帮,當(dāng)計(jì)數(shù)器為0是局嘁,鎖被釋放溉箕。如果獲取對(duì)象的鎖失敗,那當(dāng)前線程就要阻塞等待悦昵,直到對(duì)象鎖被另外一個(gè)線程釋放為止肴茄。
???在虛擬機(jī)規(guī)范對(duì)monitorenter和monitorexit的行為描述中,有兩點(diǎn)需要特別注意的但指。首先寡痰,synchronized同步塊對(duì)同一個(gè)線程來說是可重入的,不會(huì)出現(xiàn)自己把自己死鎖的問題棋凳。其次拦坠,同步塊在已進(jìn)入的線程執(zhí)行完之前,會(huì)阻塞后面其他線程的進(jìn)入
Java對(duì)象頭
鎖存在Java對(duì)象頭里剩岳。如果對(duì)象是數(shù)組類型贞滨,則虛擬機(jī)用3個(gè)Word(字寬)存儲(chǔ)對(duì)象頭,如果對(duì)象是非數(shù)組類型卢肃,則用2字寬存儲(chǔ)對(duì)象頭疲迂。在32位虛擬機(jī)中,一字寬等于四字節(jié)莫湘,即32bit尤蒿。
長度 | 內(nèi)容 | 說明 |
---|---|---|
32/64bit | Mark Word | 存儲(chǔ)對(duì)象的hashCode或鎖信息等。 |
32/64bit | Class Metadata Address | 存儲(chǔ)到對(duì)象類型數(shù)據(jù)的指針 |
32/64bit | Array length | 數(shù)組的長度(如果當(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)志位 | |
---|---|---|---|---|
無鎖狀態(tài) | 對(duì)象的hashCode | 對(duì)象分代年齡 | 0 | 01 |
在運(yùn)行期間Mark Word里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化。Mark Word可能變化為存儲(chǔ)以下4種數(shù)據(jù):
在64位虛擬機(jī)下忙芒,Mark Word是64bit大小的示弓,其存儲(chǔ)結(jié)構(gòu)如下:
Monitor
???什么是Monitor?我們可以把它理解為一個(gè)同步工具呵萨,也可以描述為一種同步機(jī)制奏属,它通常被描述為一個(gè)對(duì)象。
???與一切皆對(duì)象一樣潮峦,所有的Java對(duì)象是天生的Monitor囱皿,每一個(gè)Java對(duì)象都有成為Monitor的潛質(zhì),因?yàn)樵贘ava的設(shè)計(jì)中 忱嘹,每一個(gè)Java對(duì)象自打娘胎里出來就帶了一把看不見的鎖嘱腥,它叫做內(nèi)部鎖或者M(jìn)onitor鎖。
Monitor 是線程私有的數(shù)據(jù)結(jié)構(gòu)拘悦,每一個(gè)線程都有一個(gè)可用monitor record列表齿兔,同時(shí)還有一個(gè)全局的可用列表。每一個(gè)被鎖住的對(duì)象都會(huì)和一個(gè)monitor關(guān)聯(lián)(對(duì)象頭的MarkWord中的LockWord指向monitor的起始地址),同時(shí)monitor中有一個(gè)Owner字段存放擁有該鎖的線程的唯一標(biāo)識(shí)分苇,表示該鎖被這個(gè)線程占用添诉。其結(jié)構(gòu)如下:
- Owner:初始時(shí)為NULL表示當(dāng)前沒有任何線程擁有該monitor record,當(dāng)線程成功擁有該鎖后保存線程唯一標(biāo)識(shí)组砚,當(dāng)鎖被釋放時(shí)又設(shè)置為NULL吻商;
- EntryQ:關(guān)聯(lián)一個(gè)系統(tǒng)互斥鎖(semaphore)掏颊,阻塞所有試圖鎖住monitor record失敗的線程糟红。
- RcThis:表示blocked或waiting在該monitor record上的所有線程的個(gè)數(shù)。
- Nest:用來實(shí)現(xiàn)重入鎖的計(jì)數(shù)乌叶。
- HashCode:保存從對(duì)象頭拷貝過來的HashCode值(可能還包含GC age)盆偿。
- Candidate:用來避免不必要的阻塞或等待線程喚醒,因?yàn)槊恳淮沃挥幸粋€(gè)線程能夠成功擁有鎖准浴,如果每次前一個(gè)釋放鎖的線程喚醒所有正在阻塞或等待的線程事扭,會(huì)引起不必要的上下文切換(從阻塞到就緒然后因?yàn)楦偁庢i失敗又被阻塞)從而導(dǎo)致性能嚴(yán)重下降。Candidate只有兩種可能的值0表示沒有需要喚醒的線程1表示要喚醒一個(gè)繼任線程來競爭鎖乐横。
鎖性能優(yōu)化
自旋鎖與自適應(yīng)自旋
???如果物理機(jī)有超過一個(gè)以上的處理器求橄,讓后面請(qǐng)求鎖的那個(gè)線程“稍等一下”,但不放棄處理器的執(zhí)行時(shí)間葡公,看看持有鎖的線程是否很快就會(huì)釋放鎖罐农。為了讓線程等待,我們只需讓線程執(zhí)行一個(gè)忙循環(huán)(自旋)催什,這項(xiàng)技術(shù)就是所謂的自旋鎖
自旋次數(shù)默認(rèn)是10次涵亏,參數(shù)-XX:PreBlockSpin來更改
在JDK1.6中引入了自適應(yīng)的自旋鎖。自適應(yīng)意味著自旋的時(shí)間不再固定蒲凶,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的
鎖消除
???虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí)气筋,對(duì)一些代碼上要求同步,但是被檢測到不可能存在共享數(shù)據(jù)競爭的鎖進(jìn)行消除旋圆。鎖消除的主要判斷依據(jù)來源于逃逸分析的數(shù)據(jù)支撐宠默,如果判斷在一段代碼中,堆上的所有數(shù)據(jù)都不會(huì)逃逸出去從而被其他線程鎖訪問到灵巧,那就可以把它們當(dāng)做棧上數(shù)據(jù)對(duì)待搀矫,認(rèn)為它們是線程私有的,無須加鎖
鎖粗化
???如果一系統(tǒng)的連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖孩等,甚至加鎖操作是出現(xiàn)在循環(huán)體中艾君,那即使沒有線程競爭,頻繁地進(jìn)行互斥同步操作也會(huì)導(dǎo)致不必要的性能損耗肄方,如果虛擬機(jī)探測到有這樣一串零碎的操作都對(duì)同一個(gè)對(duì)象加鎖冰垄,將會(huì)把加鎖同步的范圍擴(kuò)展(粗化)到整個(gè)操作序列的外部
輕量級(jí)鎖
???輕量級(jí)鎖時(shí)JDK1.6之后加入的新型鎖機(jī)制,它名字中的“輕量級(jí)”是相對(duì)于使用操作系統(tǒng)互斥量來實(shí)現(xiàn)的傳統(tǒng)鎖而言的,因此傳統(tǒng)的鎖機(jī)制就稱為“重量級(jí)”鎖虹茶。首先需要強(qiáng)調(diào)一點(diǎn)的是逝薪,輕量級(jí)鎖并不是用來代替重量級(jí)鎖的,它的本意是在沒有多線程競爭的前提下蝴罪,減少傳統(tǒng)的重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗董济。
獲取輕量級(jí)鎖步驟:
1)在代碼進(jìn)入同步塊的時(shí)候,檢查此同步對(duì)象有沒有被鎖定(鎖標(biāo)志狀態(tài)為“01”)要门,若沒有被鎖定虏肾,虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的Mark Word的拷貝(官方把這份拷貝加了一個(gè)Displaced前綴欢搜,即Displaced Mark Word)封豪,否則執(zhí)行步驟3)
2)虛擬機(jī)將使用CAS操作嘗試將對(duì)象的Mark Word更新為指向Lock Record的指針,如果這個(gè)動(dòng)作更新成功炒瘟,那么這個(gè)線程就擁有了該對(duì)象的鎖吹埠,并且對(duì)象Mark Word的鎖標(biāo)志位(Mark Word的最后2bit)將轉(zhuǎn)變?yōu)椤?0”,即表示此對(duì)象處于輕量級(jí)鎖定狀態(tài)疮装,否則執(zhí)行步驟3)
3)虛擬機(jī)首先檢查對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀缘琅,如果指向,說明當(dāng)前線程已經(jīng)擁有這個(gè)對(duì)象的鎖廓推,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行刷袍,否則說這個(gè)鎖對(duì)象已經(jīng)被其他線程搶占了。如果有兩條以上的線程爭用同一個(gè)鎖受啥,那輕量級(jí)鎖就不再有效做个,要膨脹為重量級(jí)鎖,鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”滚局,Mark Word中存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針居暖,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)
解除輕量級(jí)鎖步驟:
輕量級(jí)鎖的釋放也是通過CAS操作來進(jìn)行的,主要步驟如下:
1)取出在獲取輕量級(jí)鎖保存在Displaced Mark Word中的數(shù)據(jù)藤肢;
2)用CAS操作將取出的數(shù)據(jù)替換當(dāng)前對(duì)象的Mark
Word中太闺,如果成功,則說明釋放鎖成功嘁圈,否則執(zhí)行(3)省骂;
3)如果CAS操作替換失敗,說明有其他線程嘗試獲取該鎖最住,則需要在釋放鎖的同時(shí)需要喚醒被掛起的線程钞澳。
偏向鎖
???偏向鎖也是JDK1.6中引入的一項(xiàng)鎖優(yōu)化,它的目的是消除數(shù)據(jù)在無競爭情況下的同步原語涨缚,進(jìn)一步提高程序的運(yùn)行性能轧粟。如果說輕量級(jí)鎖是在無競爭的情況下使用CAS操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個(gè)同步都消除了,連CAS操作都不做了
???偏向鎖的“偏”兰吟,就是偏心的“偏”通惫、偏袒的“偏”,它的意思是這個(gè)鎖會(huì)偏向于第一個(gè)獲得它的線程混蔼,如果在接下來的執(zhí)行過程中履腋,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠(yuǎn)不需要再進(jìn)行同步惭嚣。
獲取鎖
1)檢測Mark Word是否為可偏向狀態(tài)遵湖,即是否為偏向鎖1,鎖標(biāo)識(shí)位為01料按;
2)若為可偏向狀態(tài)奄侠,則測試線程ID是否為當(dāng)前線程ID,如果是载矿,則執(zhí)行步驟(5),否則執(zhí)行步驟(3)烹卒;
3)如果線程ID不為當(dāng)前線程ID闷盔,則通過CAS操作競爭鎖,競爭成功旅急,則將Mark Word的線程ID替換為當(dāng)前線程ID逢勾,否則執(zhí)行線程(4);
4)通過CAS競爭鎖失敗藐吮,證明當(dāng)前存在多線程競爭情況溺拱,當(dāng)?shù)竭_(dá)全局安全點(diǎn),獲得偏向鎖的線程被掛起谣辞,偏向鎖升級(jí)為輕量級(jí)鎖迫摔,然后被阻塞在安全點(diǎn)的線程繼續(xù)往下執(zhí)行同步代碼塊;
5)執(zhí)行同步代碼塊
釋放鎖
???偏向鎖的釋放采用了一種只有競爭才會(huì)釋放鎖的機(jī)制泥从,線程是不會(huì)主動(dòng)去釋放偏向鎖句占,需要等待其他線程來競爭。偏向鎖的撤銷需要等待全局安全點(diǎn)(這個(gè)時(shí)間點(diǎn)是上沒有正在執(zhí)行的代碼)躯嫉。其步驟如下:
1)暫停擁有偏向鎖的線程纱烘,判斷鎖對(duì)象石是否還處于被鎖定狀態(tài);
2)撤銷偏向蘇祈餐,恢復(fù)到無鎖狀態(tài)(01)或者輕量級(jí)鎖的狀態(tài)擂啥;
重量級(jí)鎖
重量級(jí)鎖通過對(duì)象內(nèi)部的監(jiān)視器(monitor)實(shí)現(xiàn),其中monitor的本質(zhì)是依賴于底層操作系統(tǒng)的Mutex Lock實(shí)現(xiàn)帆阳,操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài)的切換哺壶,切換成本非常高。
鎖的優(yōu)缺點(diǎn)對(duì)比
鎖 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場景 | |
---|---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法比僅存在納秒級(jí)的差距变骡。 | 如果線程間存在鎖競爭离赫,會(huì)帶來額外的鎖撤銷的消耗。 | 適用于只有一個(gè)線程訪問同步塊場景塌碌。 | |
輕量級(jí)鎖 | 競爭的線程不會(huì)阻塞渊胸,提高了程序的響應(yīng)速度。 | 如果始終得不到鎖競爭的線程使用自旋會(huì)消耗CPU台妆。 | 追求響應(yīng)時(shí)間翎猛。同步塊執(zhí)行速度非常快接剩。 | |
重量級(jí)鎖 | 線程競爭不使用自旋切厘,不會(huì)消耗CPU。 | 線程阻塞懊缺,響應(yīng)時(shí)間緩慢疫稿。 | 追求吞吐量。 | 同步塊執(zhí)行速度較長鹃两。 |
參考資料
周志明:《深入理解Java虛擬機(jī)》
方騰飛:《Java并發(fā)編程的藝術(shù)》