關於synchronized
的底層實現,網上有不少文章了。可是不少文章要麼做者根本沒看代碼,僅僅是根據網上其餘文章總結、照搬而成,不免有些錯誤;要麼不少點都是一筆帶過,對於爲何這樣實現沒有一個說法,讓像我這樣的讀者意猶未盡。java
本系列文章將對HotSpot的synchronized
鎖實現進行全面分析,內容包括偏向鎖、輕量級鎖、重量級鎖的加鎖、解鎖、鎖升級流程的原理及源碼分析,但願給在研究synchronized
路上的同窗一些幫助。linux
大概花費了兩週的實現看代碼(花費了這麼久時間有些懺愧,主要是對C++、JVM底層機制、JVM調試以及彙編代碼不太熟),將synchronized
涉及到的代碼基本都看了一遍,其中還包括在JVM中添加日誌驗證本身的猜測,總的來講目前對synchronized
這塊有了一個比較全面清晰的認識,但水平有限,有些細節不免有些疏漏,還望請你們指正。git
本篇文章將對synchronized
機制作個大體的介紹,包括用以承載鎖狀態的對象頭、鎖的幾種形式、各類形式鎖的加鎖和解鎖流程、何時會發生鎖升級。須要注意的是本文旨在介紹背景和概念,在講述一些流程的時候,只提到了主要case,對於實現細節、運行時的不一樣分支都在後面的文章中詳細分析。github
本人看的JVM版本是jdk8u,具體版本號以及代碼能夠在這裏看到。數組
Java中提供了兩種實現同步的基礎語義: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"); } }
當SyncTest.java被編譯成class文件的時候,synchronized
關鍵字和synchronized
方法的字節碼略有不一樣,咱們能夠用javap -v
命令查看class文件對應的JVM字節碼信息,部分信息以下:多線程
{ 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指令進入同步塊 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標記 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
關鍵字而言,javac
在編譯時,會生成對應的monitorenter
和monitorexit
指令分別對應synchronized
同步塊的進入和退出,有兩個monitorexit
指令的緣由是:爲了保證拋異常的狀況下也能釋放鎖,因此javac
爲同步代碼塊添加了一個隱式的try-finally,在finally中會調用monitorexit
命令釋放鎖。而對於synchronized
方法而言,javac
爲其生成了一個ACC_SYNCHRONIZED
關鍵字,在JVM進行方法調用時,發現調用的方法被ACC_SYNCHRONIZED
修飾,則會先嚐試得到鎖。oracle
在JVM底層,對於這兩種synchronized
語義的實現大體相同,在後文中會選擇一種進行詳細分析。ide
由於本文旨在分析synchronized
的實現原理,所以對於其使用的一些問題就不贅述了,不瞭解的朋友能夠看看這篇文章。函數
傳統的鎖(也就是下文要說的重量級鎖)依賴於系統的同步函數,在linux上使用mutex
互斥鎖,最底層實現依賴於futex
,關於futex
能夠看我以前的文章,這些同步函數都涉及到用戶態和內核態的切換、進程的上下文切換,成本較高。對於加了synchronized
關鍵字但運行時並無多線程競爭,或兩個線程接近於交替執行的狀況,使用傳統鎖機制無疑效率是會比較低的。
在JDK 1.6以前,synchronized
只有傳統的鎖機制,所以給開發者留下了synchronized
關鍵字相比於其餘同步機制性能很差的印象。
在JDK 1.6引入了兩種新型鎖機制:偏向鎖和輕量級鎖,它們的引入是爲了解決在沒有多線程競爭或基本沒有競爭的場景下因使用傳統鎖機制帶來的性能開銷問題。
在看這幾種鎖機制的實現前,咱們先來了解下對象頭,它是實現多種鎖機制的基礎。
由於在Java中任意對象均可以用做鎖,所以一定要有一個映射關係,存儲該對象以及其對應的鎖信息(好比當前哪一個線程持有鎖,哪些線程在等待)。一種很直觀的方法是,用一個全局map,來存儲這個映射關係,但這樣會有一些問題:須要對map作線程安全保障,不一樣的synchronized
之間會相互影響,性能差;另外當同步對象較多時,該map可能會佔用比較多的內存。
因此最好的辦法是將這個映射關係存儲在對象頭中,由於對象頭自己也有一些hashcode、GC相關的數據,因此若是能將鎖信息與這些信息共存在對象頭中就行了。
在JVM中,對象在內存中除了自己的數據外還會有個對象頭,對於普通對象而言,其對象頭中有兩類信息:mark word
和類型指針。另外對於數組而言還會有一份記錄數組長度的數據。
類型指針是指向該對象所屬類對象的指針,mark word
用於存儲對象的HashCode、GC分代年齡、鎖狀態等信息。在32位系統上mark word
長度爲32bit,64位系統上長度爲64bit。爲了能在有限的空間裏存儲下更多的數據,其存儲格式是不固定的,在32位系統上各狀態的格式以下:
能夠看到鎖信息也是存在於對象的mark word
中的。當對象狀態爲偏向鎖(biasable)時,mark word
存儲的是偏向的線程ID;當狀態爲輕量級鎖(lightweight locked)時,mark word
存儲的是指向線程棧中Lock Record
的指針;當狀態爲重量級鎖(inflated)時,爲指向堆中的monitor對象的指針。
重量級鎖是咱們常說的傳統意義上的鎖,其利用操做系統底層的同步機制去實現Java中的線程同步。
重量級鎖的狀態下,對象的mark word
爲指向一個堆中monitor對象的指針。
一個monitor對象包括這麼幾個關鍵字段:cxq(下圖中的ContentionList),EntryList ,WaitSet,owner。
其中cxq ,EntryList ,WaitSet都是由ObjectWaiter的鏈表結構,owner指向持有鎖的線程。
當一個線程嘗試得到鎖時,若是該鎖已經被佔用,則會將該線程封裝成一個ObjectWaiter對象插入到cxq的隊列尾部,而後暫停當前線程。當持有鎖的線程釋放鎖前,會將cxq中的全部元素移動到EntryList中去,並喚醒EntryList的隊首線程。
若是一個線程在同步塊中調用了Object#wait
方法,會將該線程對應的ObjectWaiter從EntryList移除並加入到WaitSet中,而後釋放鎖。當wait的線程被notify以後,會將對應的ObjectWaiter從WaitSet移動到EntryList中。
以上只是對重量級鎖流程的一個簡述,其中涉及到的不少細節,好比ObjectMonitor對象從哪來?釋放鎖時是將cxq中的元素移動到EntryList的尾部仍是頭部?notfiy時,是將ObjectWaiter移動到EntryList的尾部仍是頭部?
關於具體的細節,會在重量級鎖的文章中分析。
JVM的開發者發如今不少狀況下,在Java程序運行時,同步塊中的代碼都是不存在競爭的,不一樣的線程交替的執行同步塊中的代碼。這種狀況下,用重量級鎖是不必的。所以JVM引入了輕量級鎖的概念。
線程在執行同步塊以前,JVM會先在當前的線程的棧幀中建立一個Lock Record
,其包括一個用於存儲對象頭中的 mark word
(官方稱之爲Displaced Mark Word
)以及一個指向對象的指針。下圖右邊的部分就是一個Lock Record
。
1.在線程棧中建立一個Lock Record
,將其obj
(即上圖的Object reference)字段指向鎖對象。
2.直接經過CAS指令將Lock Record
的地址存儲在對象頭的mark word
中,若是對象處於無鎖狀態則修改爲功,表明該線程得到了輕量級鎖。若是失敗,進入到步驟3。
3.若是是當前線程已經持有該鎖了,表明這是一次鎖重入。設置Lock Record
第一部分(Displaced Mark Word
)爲null,起到了一個重入計數器的做用。而後結束。
4.走到這一步說明發生了競爭,須要膨脹爲重量級鎖。
1.遍歷線程棧,找到全部obj
字段等於當前鎖對象的Lock Record
。
2.若是Lock Record
的Displaced Mark Word
爲null,表明這是一次重入,將obj
設置爲null後continue。
3.若是Lock Record
的Displaced Mark Word
不爲null,則利用CAS指令將對象頭的mark word
恢復成爲Displaced Mark Word
。若是成功,則continue,不然膨脹爲重量級鎖。
Java是支持多線程的語言,所以在不少二方包、基礎庫中爲了保證代碼在多線程的狀況下也能正常運行,也就是咱們常說的線程安全,都會加入如synchronized
這樣的同步語義。可是在應用在實際運行時,極可能只有一個線程會調用相關同步方法。好比下面這個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
的修飾,但實際使用時卻只有一個線程調用到該方法,對於輕量級鎖而言,每次調用addString時,加鎖解鎖都有一個CAS操做;對於重量級鎖而言,加鎖也會有一個或多個CAS操做(這裏的’一個‘、’多個‘數量詞只是針對該demo,並不適用於全部場景)。
在JDK1.6中爲了提升一個對象在一段很長的時間內都只被一個線程用作鎖對象場景下的性能,引入了偏向鎖,在第一次得到鎖時,會有一個CAS操做,以後該線程再獲取鎖,只會執行幾個簡單的命令,而不是開銷相對較大的CAS命令。咱們來看看偏向鎖是如何作的。
當JVM啓用了偏向鎖模式(1.6以上默認開啓),當新建立一個對象的時候,若是該對象所屬的class沒有關閉偏向鎖模式(何時會關閉一個class的偏向模式下文會說,默認全部class的偏向模式都是是開啓的),那新建立對象的mark word
將是可偏向狀態,此時mark word中
的thread id(參見上文偏向狀態下的mark word
格式)爲0,表示未偏向任何線程,也叫作匿名偏向(anonymously biased)。
case 1:當該對象第一次被線程得到鎖的時候,發現是匿名偏向狀態,則會用CAS指令,將mark word
中的thread id由0改爲當前線程Id。若是成功,則表明得到了偏向鎖,繼續執行同步塊中的代碼。不然,將偏向鎖撤銷,升級爲輕量級鎖。
case 2:當被偏向的線程再次進入同步塊時,發現鎖對象偏向的就是當前線程,在經過一些額外的檢查後(細節見後面的文章),會往當前線程的棧中添加一條Displaced Mark Word
爲空的Lock Record
中,而後繼續執行同步塊的代碼,由於操縱的是線程私有的棧,所以不須要用到CAS指令;因而可知偏向鎖模式下,當被偏向的線程再次嘗試得到鎖時,僅僅進行幾個簡單的操做就能夠了,在這種狀況下,synchronized
關鍵字帶來的性能開銷基本能夠忽略。
case 3.當其餘線程進入同步塊時,發現已經有偏向的線程了,則會進入到撤銷偏向鎖的邏輯裏,通常來講,會在safepoint
中去查看偏向的線程是否還存活,若是存活且還在同步塊中則將鎖升級爲輕量級鎖,原偏向的線程繼續擁有鎖,當前線程則走入到鎖升級的邏輯裏;若是偏向的線程已經不存活或者不在同步塊中,則將對象頭的mark word
改成無鎖狀態(unlocked),以後再升級爲輕量級鎖。
因而可知,偏向鎖升級的時機爲:當鎖已經發生偏向後,只要有另外一個線程嘗試得到偏向鎖,則該偏向鎖就會升級成輕量級鎖。固然這個說法不絕對,由於還有批量重偏向這一機制。
當有其餘線程嘗試得到鎖時,是根據遍歷偏向線程的lock record
來肯定該線程是否還在執行同步塊中的代碼。所以偏向鎖的解鎖很簡單,僅僅將棧中的最近一條lock record
的obj
字段設置爲null。須要注意的是,偏向鎖的解鎖步驟中並不會修改對象頭中的thread id。
下圖展現了鎖狀態的轉換流程:
另外,偏向鎖默認不是當即就啓動的,在程序啓動後,一般有幾秒的延遲,能夠經過命令 -XX:BiasedLockingStartupDelay=0
來關閉延遲。
從上文偏向鎖的加鎖解鎖過程當中能夠看出,當只有一個線程反覆進入同步塊時,偏向鎖帶來的性能開銷基本能夠忽略,可是當有其餘線程嘗試得到鎖時,就須要等到safe point
時將偏向鎖撤銷爲無鎖狀態或升級爲輕量級/重量級鎖。safe point
這個詞咱們在GC中常常會提到,其表明了一個狀態,在該狀態下全部線程都是暫停的(大概這麼個意思),詳細能夠看這篇文章。總之,偏向鎖的撤銷是有必定成本的,若是說運行時的場景自己存在多線程競爭的,那偏向鎖的存在不只不能提升性能,並且會致使性能降低。所以,JVM中增長了一種批量重偏向/撤銷的機制。
存在以下兩種狀況:(見官方論文第4小節):
1.一個線程建立了大量對象並執行了初始的同步操做,以後在另外一個線程中將這些對象做爲鎖進行以後的操做。這種case下,會致使大量的偏向鎖撤銷操做。
2.存在明顯多線程競爭的場景下使用偏向鎖是不合適的,例如生產者/消費者隊列。
批量重偏向(bulk rebias)機制是爲了解決第一種場景。批量撤銷(bulk revoke)則是爲了解決第二種場景。
其作法是:以class爲單位,爲每一個class維護一個偏向鎖撤銷計數器,每一次該class的對象發生偏向撤銷操做時,該計數器+1,當這個值達到重偏向閾值(默認20)時,JVM就認爲該class的偏向鎖有問題,所以會進行批量重偏向。每一個class對象會有一個對應的epoch
字段,每一個處於偏向鎖狀態對象的mark word中
也有該字段,其初始值爲建立該對象時,class中的epoch
的值。每次發生批量重偏向時,就將該值+1,同時遍歷JVM中全部線程的棧,找到該class全部正處於加鎖狀態的偏向鎖,將其epoch
字段改成新值。下次得到鎖時,發現當前對象的epoch
值和class的epoch
不相等,那就算當前已經偏向了其餘線程,也不會執行撤銷操做,而是直接經過CAS操做將其mark word
的Thread Id 改爲當前線程Id。
當達到重偏向閾值後,假設該class計數器繼續增加,當其達到批量撤銷的閾值後(默認40),JVM就認爲該class的使用場景存在多線程競爭,會標記該class爲不可偏向,以後,對於該class的鎖,直接走輕量級鎖的邏輯。
Java中的synchronized
有偏向鎖、輕量級鎖、重量級鎖三種形式,分別對應了鎖只被一個線程持有、不一樣線程交替持有鎖、多線程競爭鎖三種狀況。當條件不知足時,鎖會按偏向鎖->輕量級鎖->重量級鎖 的順序升級。JVM種的鎖也是能降級的,只不過條件很苛刻,不在咱們討論範圍以內。該篇文章主要是對Java的synchronized
作個基本介紹,後文會有更詳細的分析。