原文地址:https://github.com/farmerjohngit/myblog/issues/12
關(guān)于synchronized
的底層實(shí)現(xiàn)瓢阴,網(wǎng)上有很多文章了。但是很多文章要么作者根本沒看代碼健无,僅僅是根據(jù)網(wǎng)上其他文章總結(jié)荣恐、照搬而成,難免有些錯誤累贤;要么很多點(diǎn)都是一筆帶過叠穆,對于為什么這樣實(shí)現(xiàn)沒有一個說法,讓像我這樣的讀者意猶未盡臼膏。
本系列文章將對HotSpot的synchronized
鎖實(shí)現(xiàn)進(jìn)行全面分析硼被,內(nèi)容包括偏向鎖、輕量級鎖渗磅、重量級鎖的加鎖嚷硫、解鎖、鎖升級流程的原理及源碼分析夺溢,希望給在研究synchronized
路上的同學(xué)一些幫助论巍。主要包括以下幾篇文章:
死磕Synchronized底層實(shí)現(xiàn)--概論
死磕Synchronized底層實(shí)現(xiàn)--偏向鎖
死磕Synchronized底層實(shí)現(xiàn)--輕量級鎖
死磕Synchronized底層實(shí)現(xiàn)--重量級鎖
更多文章見個人博客:https://github.com/farmerjohngit/myblog
大概花費(fèi)了兩周的實(shí)現(xiàn)看代碼(花費(fèi)了這么久時間有些懺愧,主要是對C++风响、JVM底層機(jī)制嘉汰、JVM調(diào)試以及匯編代碼不太熟),將synchronized
涉及到的代碼基本都看了一遍状勤,其中還包括在JVM中添加日志驗(yàn)證自己的猜想鞋怀,總的來說目前對synchronized
這塊有了一個比較全面清晰的認(rèn)識,但水平有限持搜,有些細(xì)節(jié)難免有些疏漏密似,還望請大家指正。
本篇文章將對synchronized
機(jī)制做個大致的介紹葫盼,包括用以承載鎖狀態(tài)的對象頭残腌、鎖的幾種形式、各種形式鎖的加鎖和解鎖流程、什么時候會發(fā)生鎖升級抛猫。需要注意的是本文旨在介紹背景和概念蟆盹,在講述一些流程的時候,只提到了主要case闺金,對于實(shí)現(xiàn)細(xì)節(jié)逾滥、運(yùn)行時的不同分支都在后面的文章中詳細(xì)分析。
本人看的JVM版本是jdk8u败匹,具體版本號以及代碼可以在這里看到寨昙。
synchronized簡介
Java中提供了兩種實(shí)現(xiàn)同步的基礎(chǔ)語義:synchronized
方法和synchronized
塊, 我們來看個demo:
public class SyncTest {
public void syncBlock(){
synchronized (this){
System.out.println("hello block");
}
}
public synchronized void syncMethod(){
System.out.println("hello method");
}
}
當(dāng)SyncTest.java被編譯成class文件的時候掀亩,synchronized
關(guān)鍵字和synchronized
方法的字節(jié)碼略有不同舔哪,我們可以用javap -v
命令查看class文件對應(yīng)的JVM字節(jié)碼信息,部分信息如下:
{
public void syncBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter // monitorenter指令進(jìn)入同步塊
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String hello block
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit // monitorexit指令退出同步塊
14: goto 22
17: astore_2
18: aload_1
19: monitorexit // monitorexit指令退出同步塊
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
public synchronized void syncMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED //添加了ACC_SYNCHRONIZED標(biāo)記
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String hello method
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
從上面的中文注釋處可以看到槽棍,對于synchronized
關(guān)鍵字而言尸红,javac
在編譯時,會生成對應(yīng)的monitorenter
和monitorexit
指令分別對應(yīng)synchronized
同步塊的進(jìn)入和退出刹泄,有兩個monitorexit
指令的原因是:為了保證拋異常的情況下也能釋放鎖,所以javac
為同步代碼塊添加了一個隱式的try-finally怎爵,在finally中會調(diào)用monitorexit
命令釋放鎖特石。而對于synchronized
方法而言,javac
為其生成了一個ACC_SYNCHRONIZED
關(guān)鍵字鳖链,在JVM進(jìn)行方法調(diào)用時姆蘸,發(fā)現(xiàn)調(diào)用的方法被ACC_SYNCHRONIZED
修飾,則會先嘗試獲得鎖芙委。
在JVM底層逞敷,對于這兩種synchronized
語義的實(shí)現(xiàn)大致相同,在后文中會選擇一種進(jìn)行詳細(xì)分析灌侣。
因?yàn)楸疚闹荚诜治?code>synchronized的實(shí)現(xiàn)原理推捐,因此對于其使用的一些問題就不贅述了,不了解的朋友可以看看這篇文章侧啼。
鎖的幾種形式
傳統(tǒng)的鎖(也就是下文要說的重量級鎖)依賴于系統(tǒng)的同步函數(shù)牛柒,在linux上使用mutex
互斥鎖,最底層實(shí)現(xiàn)依賴于futex
痊乾,關(guān)于futex
可以看我之前的文章皮壁,這些同步函數(shù)都涉及到用戶態(tài)和內(nèi)核態(tài)的切換、進(jìn)程的上下文切換哪审,成本較高蛾魄。對于加了synchronized
關(guān)鍵字但運(yùn)行時并沒有多線程競爭,或兩個線程接近于交替執(zhí)行的情況,使用傳統(tǒng)鎖機(jī)制無疑效率是會比較低的滴须。
在JDK 1.6之前,synchronized
只有傳統(tǒng)的鎖機(jī)制舌狗,因此給開發(fā)者留下了synchronized
關(guān)鍵字相比于其他同步機(jī)制性能不好的印象。
在JDK 1.6引入了兩種新型鎖機(jī)制:偏向鎖和輕量級鎖描馅,它們的引入是為了解決在沒有多線程競爭或基本沒有競爭的場景下因使用傳統(tǒng)鎖機(jī)制帶來的性能開銷問題把夸。
在看這幾種鎖機(jī)制的實(shí)現(xiàn)前,我們先來了解下對象頭铭污,它是實(shí)現(xiàn)多種鎖機(jī)制的基礎(chǔ)恋日。
對象頭
因?yàn)樵贘ava中任意對象都可以用作鎖,因此必定要有一個映射關(guān)系嘹狞,存儲該對象以及其對應(yīng)的鎖信息(比如當(dāng)前哪個線程持有鎖岂膳,哪些線程在等待)。一種很直觀的方法是磅网,用一個全局map谈截,來存儲這個映射關(guān)系,但這樣會有一些問題:需要對map做線程安全保障涧偷,不同的synchronized
之間會相互影響簸喂,性能差;另外當(dāng)同步對象較多時燎潮,該map可能會占用比較多的內(nèi)存喻鳄。
所以最好的辦法是將這個映射關(guān)系存儲在對象頭中,因?yàn)閷ο箢^本身也有一些hashcode确封、GC相關(guān)的數(shù)據(jù)除呵,所以如果能將鎖信息與這些信息共存在對象頭中就好了。
在JVM中爪喘,對象在內(nèi)存中除了本身的數(shù)據(jù)外還會有個對象頭颜曾,對于普通對象而言,其對象頭中有兩類信息:mark word
和類型指針秉剑。另外對于數(shù)組而言還會有一份記錄數(shù)組長度的數(shù)據(jù)泛豪。
類型指針是指向該對象所屬類對象的指針,mark word
用于存儲對象的HashCode秃症、GC分代年齡候址、鎖狀態(tài)等信息。在32位系統(tǒng)上mark word
長度為32bit种柑,64位系統(tǒng)上長度為64bit岗仑。為了能在有限的空間里存儲下更多的數(shù)據(jù),其存儲格式是不固定的聚请,在32位系統(tǒng)上各狀態(tài)的格式如下:
可以看到鎖信息也是存在于對象的mark word
中的荠雕。當(dāng)對象狀態(tài)為偏向鎖(biasable)時稳其,mark word
存儲的是偏向的線程ID;當(dāng)狀態(tài)為輕量級鎖(lightweight locked)時炸卑,mark word
存儲的是指向線程棧中Lock Record
的指針既鞠;當(dāng)狀態(tài)為重量級鎖(inflated)時,為指向堆中的monitor對象的指針盖文。
重量級鎖
重量級鎖是我們常說的傳統(tǒng)意義上的鎖嘱蛋,其利用操作系統(tǒng)底層的同步機(jī)制去實(shí)現(xiàn)Java中的線程同步。
重量級鎖的狀態(tài)下五续,對象的mark word
為指向一個堆中monitor對象的指針洒敏。
一個monitor對象包括這么幾個關(guān)鍵字段:cxq(下圖中的ContentionList),EntryList 疙驾,WaitSet凶伙,owner。
其中cxq 它碎,EntryList 函荣,WaitSet都是由ObjectWaiter的鏈表結(jié)構(gòu),owner指向持有鎖的線程扳肛。
當(dāng)一個線程嘗試獲得鎖時傻挂,如果該鎖已經(jīng)被占用,則會將該線程封裝成一個ObjectWaiter對象插入到cxq的隊(duì)列尾部挖息,然后暫停當(dāng)前線程踊谋。當(dāng)持有鎖的線程釋放鎖前,會將cxq中的所有元素移動到EntryList中去旋讹,并喚醒EntryList的隊(duì)首線程。
如果一個線程在同步塊中調(diào)用了Object#wait
方法轿衔,會將該線程對應(yīng)的ObjectWaiter從EntryList移除并加入到WaitSet中沉迹,然后釋放鎖。當(dāng)wait的線程被notify之后害驹,會將對應(yīng)的ObjectWaiter從WaitSet移動到EntryList中鞭呕。
以上只是對重量級鎖流程的一個簡述,其中涉及到的很多細(xì)節(jié)宛官,比如ObjectMonitor對象從哪來葫松?釋放鎖時是將cxq中的元素移動到EntryList的尾部還是頭部?notfiy時底洗,是將ObjectWaiter移動到EntryList的尾部還是頭部腋么?
關(guān)于具體的細(xì)節(jié),會在重量級鎖的文章中分析亥揖。
輕量級鎖
JVM的開發(fā)者發(fā)現(xiàn)在很多情況下珊擂,在Java程序運(yùn)行時圣勒,同步塊中的代碼都是不存在競爭的,不同的線程交替的執(zhí)行同步塊中的代碼摧扇。這種情況下圣贸,用重量級鎖是沒必要的。因此JVM引入了輕量級鎖的概念扛稽。
線程在執(zhí)行同步塊之前吁峻,JVM會先在當(dāng)前的線程的棧幀中創(chuàng)建一個Lock Record
,其包括一個用于存儲對象頭中的 mark word
(官方稱之為Displaced Mark Word
)以及一個指向?qū)ο蟮闹羔樤谡拧O聢D右邊的部分就是一個Lock Record
用含。
加鎖過程
1.在線程棧中創(chuàng)建一個Lock Record
,將其obj
(即上圖的Object reference)字段指向鎖對象瞧掺。
2.直接通過CAS指令將Lock Record
的地址存儲在對象頭的mark word
中耕餐,如果對象處于無鎖狀態(tài)則修改成功,代表該線程獲得了輕量級鎖辟狈。如果失敗肠缔,進(jìn)入到步驟3。
3.如果是當(dāng)前線程已經(jīng)持有該鎖了哼转,代表這是一次鎖重入明未。設(shè)置Lock Record
第一部分(Displaced Mark Word
)為null,起到了一個重入計數(shù)器的作用壹蔓。然后結(jié)束趟妥。
4.走到這一步說明發(fā)生了競爭,需要膨脹為重量級鎖佣蓉。
解鎖過程
1.遍歷線程棧,找到所有obj
字段等于當(dāng)前鎖對象的Lock Record
披摄。
2.如果Lock Record
的Displaced Mark Word
為null,代表這是一次重入勇凭,將obj
設(shè)置為null后continue疚膊。
3.如果Lock Record
的Displaced Mark Word
不為null,則利用CAS指令將對象頭的mark word
恢復(fù)成為Displaced Mark Word
虾标。如果成功寓盗,則continue,否則膨脹為重量級鎖璧函。
偏向鎖
Java是支持多線程的語言傀蚌,因此在很多二方包、基礎(chǔ)庫中為了保證代碼在多線程的情況下也能正常運(yùn)行蘸吓,也就是我們常說的線程安全善炫,都會加入如synchronized
這樣的同步語義。但是在應(yīng)用在實(shí)際運(yùn)行時库继,很可能只有一個線程會調(diào)用相關(guān)同步方法销部。比如下面這個demo:
import java.util.ArrayList;
import java.util.List;
public class SyncDemo1 {
public static void main(String[] args) {
SyncDemo1 syncDemo1 = new SyncDemo1();
for (int i = 0; i < 100; i++) {
syncDemo1.addString("test:" + i);
}
}
private List<String> list = new ArrayList<>();
public synchronized void addString(String s) {
list.add(s);
}
}
在這個demo中為了保證對list操縱時線程安全摸航,對addString方法加了synchronized
的修飾,但實(shí)際使用時卻只有一個線程調(diào)用到該方法舅桩,對于輕量級鎖而言酱虎,每次調(diào)用addString時,加鎖解鎖都有一個CAS操作擂涛;對于重量級鎖而言读串,加鎖也會有一個或多個CAS操作(這里的’一個‘、’多個‘?dāng)?shù)量詞只是針對該demo撒妈,并不適用于所有場景)恢暖。
在JDK1.6中為了提高一個對象在一段很長的時間內(nèi)都只被一個線程用做鎖對象場景下的性能,引入了偏向鎖狰右,在第一次獲得鎖時杰捂,會有一個CAS操作,之后該線程再獲取鎖棋蚌,只會執(zhí)行幾個簡單的命令嫁佳,而不是開銷相對較大的CAS命令。我們來看看偏向鎖是如何做的谷暮。
對象創(chuàng)建
當(dāng)JVM啟用了偏向鎖模式(1.6以上默認(rèn)開啟)蒿往,當(dāng)新創(chuàng)建一個對象的時候,如果該對象所屬的class沒有關(guān)閉偏向鎖模式(什么時候會關(guān)閉一個class的偏向模式下文會說湿弦,默認(rèn)所有class的偏向模式都是是開啟的)瓤漏,那新創(chuàng)建對象的mark word
將是可偏向狀態(tài),此時mark word中
的thread id(參見上文偏向狀態(tài)下的mark word
格式)為0颊埃,表示未偏向任何線程蔬充,也叫做匿名偏向(anonymously biased)。
加鎖過程
case 1:當(dāng)該對象第一次被線程獲得鎖的時候班利,發(fā)現(xiàn)是匿名偏向狀態(tài)娃惯,則會用CAS指令,將mark word
中的thread id由0改成當(dāng)前線程Id肥败。如果成功,則代表獲得了偏向鎖愕提,繼續(xù)執(zhí)行同步塊中的代碼馒稍。否則,將偏向鎖撤銷浅侨,升級為輕量級鎖纽谒。
case 2:當(dāng)被偏向的線程再次進(jìn)入同步塊時,發(fā)現(xiàn)鎖對象偏向的就是當(dāng)前線程如输,在通過一些額外的檢查后(細(xì)節(jié)見后面的文章)鼓黔,會往當(dāng)前線程的棧中添加一條Displaced Mark Word
為空的Lock Record
中央勒,然后繼續(xù)執(zhí)行同步塊的代碼,因?yàn)椴倏v的是線程私有的棧澳化,因此不需要用到CAS指令崔步;由此可見偏向鎖模式下,當(dāng)被偏向的線程再次嘗試獲得鎖時缎谷,僅僅進(jìn)行幾個簡單的操作就可以了井濒,在這種情況下,synchronized
關(guān)鍵字帶來的性能開銷基本可以忽略列林。
case 3.當(dāng)其他線程進(jìn)入同步塊時瑞你,發(fā)現(xiàn)已經(jīng)有偏向的線程了,則會進(jìn)入到撤銷偏向鎖的邏輯里希痴,一般來說者甲,會在safepoint
中去查看偏向的線程是否還存活,如果存活且還在同步塊中則將鎖升級為輕量級鎖砌创,原偏向的線程繼續(xù)擁有鎖虏缸,當(dāng)前線程則走入到鎖升級的邏輯里;如果偏向的線程已經(jīng)不存活或者不在同步塊中纺铭,則將對象頭的mark word
改為無鎖狀態(tài)(unlocked)寇钉,之后再升級為輕量級鎖。
由此可見舶赔,偏向鎖升級的時機(jī)為:當(dāng)鎖已經(jīng)發(fā)生偏向后扫倡,只要有另一個線程嘗試獲得偏向鎖惜索,則該偏向鎖就會升級成輕量級鎖嘹屯。當(dāng)然這個說法不絕對腕够,因?yàn)檫€有批量重偏向這一機(jī)制坛怪。
解鎖過程
當(dāng)有其他線程嘗試獲得鎖時客年,是根據(jù)遍歷偏向線程的lock record
來確定該線程是否還在執(zhí)行同步塊中的代碼挡毅。因此偏向鎖的解鎖很簡單氮惯,僅僅將棧中的最近一條lock record
的obj
字段設(shè)置為null绍刮。需要注意的是桶略,偏向鎖的解鎖步驟中并不會修改對象頭中的thread id语淘。
下圖展示了鎖狀態(tài)的轉(zhuǎn)換流程:
另外,偏向鎖默認(rèn)不是立即就啟動的际歼,在程序啟動后惶翻,通常有幾秒的延遲,可以通過命令 -XX:BiasedLockingStartupDelay=0
來關(guān)閉延遲鹅心。
批量重偏向與撤銷
從上文偏向鎖的加鎖解鎖過程中可以看出吕粗,當(dāng)只有一個線程反復(fù)進(jìn)入同步塊時,偏向鎖帶來的性能開銷基本可以忽略旭愧,但是當(dāng)有其他線程嘗試獲得鎖時颅筋,就需要等到safe point
時將偏向鎖撤銷為無鎖狀態(tài)或升級為輕量級/重量級鎖宙暇。safe point
這個詞我們在GC中經(jīng)常會提到,其代表了一個狀態(tài)议泵,在該狀態(tài)下所有線程都是暫停的(大概這么個意思)占贫,詳細(xì)可以看這篇文章≈荆總之靶剑,偏向鎖的撤銷是有一定成本的,如果說運(yùn)行時的場景本身存在多線程競爭的池充,那偏向鎖的存在不僅不能提高性能桩引,而且會導(dǎo)致性能下降。因此收夸,JVM中增加了一種批量重偏向/撤銷的機(jī)制坑匠。
存在如下兩種情況:(見官方論文第4小節(jié)):
1.一個線程創(chuàng)建了大量對象并執(zhí)行了初始的同步操作,之后在另一個線程中將這些對象作為鎖進(jìn)行之后的操作卧惜。這種case下厘灼,會導(dǎo)致大量的偏向鎖撤銷操作。
2.存在明顯多線程競爭的場景下使用偏向鎖是不合適的咽瓷,例如生產(chǎn)者/消費(fèi)者隊(duì)列设凹。
批量重偏向(bulk rebias)機(jī)制是為了解決第一種場景。批量撤銷(bulk revoke)則是為了解決第二種場景茅姜。
其做法是:以class為單位闪朱,為每個class維護(hù)一個偏向鎖撤銷計數(shù)器,每一次該class的對象發(fā)生偏向撤銷操作時钻洒,該計數(shù)器+1奋姿,當(dāng)這個值達(dá)到重偏向閾值(默認(rèn)20)時,JVM就認(rèn)為該class的偏向鎖有問題素标,因此會進(jìn)行批量重偏向称诗。每個class對象會有一個對應(yīng)的epoch
字段,每個處于偏向鎖狀態(tài)對象的mark word中
也有該字段头遭,其初始值為創(chuàng)建該對象時寓免,class中的epoch
的值。每次發(fā)生批量重偏向時计维,就將該值+1袜香,同時遍歷JVM中所有線程的棧,找到該class所有正處于加鎖狀態(tài)的偏向鎖享潜,將其epoch
字段改為新值。下次獲得鎖時嗅蔬,發(fā)現(xiàn)當(dāng)前對象的epoch
值和class的epoch
不相等剑按,那就算當(dāng)前已經(jīng)偏向了其他線程疾就,也不會執(zhí)行撤銷操作,而是直接通過CAS操作將其mark word
的Thread Id 改成當(dāng)前線程Id艺蝴。
當(dāng)達(dá)到重偏向閾值后猬腰,假設(shè)該class計數(shù)器繼續(xù)增長,當(dāng)其達(dá)到批量撤銷的閾值后(默認(rèn)40)猜敢,JVM就認(rèn)為該class的使用場景存在多線程競爭姑荷,會標(biāo)記該class為不可偏向,之后缩擂,對于該class的鎖鼠冕,直接走輕量級鎖的邏輯。
End
Java中的synchronized
有偏向鎖胯盯、輕量級鎖懈费、重量級鎖三種形式,分別對應(yīng)了鎖只被一個線程持有博脑、不同線程交替持有鎖憎乙、多線程競爭鎖三種情況。當(dāng)條件不滿足時叉趣,鎖會按偏向鎖->輕量級鎖->重量級鎖 的順序升級泞边。JVM種的鎖也是能降級的,只不過條件很苛刻疗杉,不在我們討論范圍之內(nèi)阵谚。該篇文章主要是對Java的synchronized
做個基本介紹,后文會有更詳細(xì)的分析乡数。