死磕Synchronized底層實現,面試你還怕什麼?

關於synchronized的底層實現,網上有不少文章了。可是不少文章要麼做者根本沒看代碼,僅僅是根據網上其餘文章總結、照搬而成,不免有些錯誤;要麼不少點都是一筆帶過,對於爲何這樣實現沒有一個說法,讓像我這樣的讀者意猶未盡。java

本系列文章將對HotSpot的synchronized鎖實現進行全面分析,內容包括偏向鎖、輕量級鎖、重量級鎖的加鎖、解鎖、鎖升級流程的原理及源碼分析,但願給在研究synchronized路上的同窗一些幫助。linux

 

大概花費了兩週的實現看代碼(花費了這麼久時間有些懺愧,主要是對C++、JVM底層機制、JVM調試以及彙編代碼不太熟),將synchronized涉及到的代碼基本都看了一遍,其中還包括在JVM中添加日誌驗證本身的猜測,總的來講目前對synchronized這塊有了一個比較全面清晰的認識,但水平有限,有些細節不免有些疏漏,還望請你們指正。git

本篇文章將對synchronized機制作個大體的介紹,包括用以承載鎖狀態的對象頭、鎖的幾種形式、各類形式鎖的加鎖和解鎖流程、何時會發生鎖升級。須要注意的是本文旨在介紹背景和概念,在講述一些流程的時候,只提到了主要case,對於實現細節、運行時的不一樣分支都在後面的文章中詳細分析github

本人看的JVM版本是jdk8u,具體版本號以及代碼能夠在這裏看到。數組

synchronized簡介

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在編譯時,會生成對應的monitorentermonitorexit指令分別對應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位系統上各狀態的格式以下:

《死磕Synchronized底層實現--概論》

能夠看到鎖信息也是存在於對象的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指向持有鎖的線程。

《死磕Synchronized底層實現--概論》

當一個線程嘗試得到鎖時,若是該鎖已經被佔用,則會將該線程封裝成一個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

《死磕Synchronized底層實現--概論》

加鎖過程

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 RecordDisplaced Mark Word爲null,表明這是一次重入,將obj設置爲null後continue。

3.若是Lock RecordDisplaced 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 recordobj字段設置爲null。須要注意的是,偏向鎖的解鎖步驟中並不會修改對象頭中的thread id。

下圖展現了鎖狀態的轉換流程:

《死磕Synchronized底層實現--概論》

另外,偏向鎖默認不是當即就啓動的,在程序啓動後,一般有幾秒的延遲,能夠經過命令 -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的鎖,直接走輕量級鎖的邏輯。

End

Java中的synchronized有偏向鎖、輕量級鎖、重量級鎖三種形式,分別對應了鎖只被一個線程持有、不一樣線程交替持有鎖、多線程競爭鎖三種狀況。當條件不知足時,鎖會按偏向鎖->輕量級鎖->重量級鎖 的順序升級。JVM種的鎖也是能降級的,只不過條件很苛刻,不在咱們討論範圍以內。該篇文章主要是對Java的synchronized作個基本介紹,後文會有更詳細的分析。

相關文章
相關標籤/搜索