Synchronized 實(shí)現(xiàn)原理

實(shí)現(xiàn)synchronized的基礎(chǔ)有兩個(gè):Java 對(duì)象頭和 Monitor。
在虛擬機(jī)規(guī)范中定義了對(duì)象在內(nèi)存中的布局,主要由以下 3 部分組成:

  • 對(duì)象頭
  • 實(shí)例數(shù)據(jù)
  • 對(duì)齊填充

synchronized的實(shí)現(xiàn)就藏在對(duì)象頭中。對(duì)象頭中由兩個(gè)比較重要的部分組成:

  • Mark Word:默認(rèn)存儲(chǔ)對(duì)象的 hashCode,分代年齡,鎖類型,鎖標(biāo)志位等信息,是實(shí)現(xiàn)輕量級(jí)鎖和偏向鎖的關(guān)鍵

  • Class Metadata Address:類型指針指向?qū)ο蟮念愒獢?shù)據(jù),JVM 通過(guò)這個(gè)指針確定該對(duì)象是哪個(gè)類的數(shù)據(jù)

下圖是在 32 位機(jī)器上的 Mark Word 的組成示意圖。在 Java6 之前,synchronized的實(shí)現(xiàn)是依靠重量級(jí)鎖來(lái)實(shí)現(xiàn)的,鎖標(biāo)志位是10。在 Java6后,對(duì)synchronized進(jìn)行了優(yōu)化,增加了輕量級(jí)鎖和偏向鎖
<div align="center"> <img src="https://user-gold-cdn.xitu.io/2020/4/6/1714e3c8c13a7ae7?w=980&h=288&f=png&s=32490"/> </div>

我們先來(lái)說(shuō)一下重量級(jí)鎖:重量級(jí)鎖中存放的是指向重量級(jí)鎖的指針。在 Java 中,每個(gè)對(duì)象都存在著一個(gè) Monitor 與之關(guān)聯(lián)。當(dāng)線程持有一個(gè)對(duì)象的 Monitor 后,Monitor便處于鎖定狀態(tài)。在 Hotspot 中,Monitor 是由ObjectMonitor來(lái)實(shí)現(xiàn)的。

點(diǎn)擊這里查看源碼

  ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

上面是ObjectMonitor類的初始化代碼,可以看到有_WaitSet_EntryList,對(duì)應(yīng)于 Java 中的等待池和鎖池

  • _owner表示的是持有objectMonitor的線程。當(dāng)多個(gè)線程同時(shí)訪問(wèn)一個(gè)對(duì)象的同步代碼時(shí),首先會(huì)進(jìn)入到_EntryList里,當(dāng)線程獲取到對(duì)象的Monitor后,就把Monitor中的owner設(shè)置位當(dāng)前線程,同時(shí)Monitor中的_count變量+1
  • 若持有Monitor的線程執(zhí)行完畢或者調(diào)用 wait()方法,則會(huì)釋放持有的Monitorowner會(huì)被設(shè)置為NULL,并將 count-1。當(dāng)前線程會(huì)進(jìn)入到_WaitSet,等待被喚醒
    Monitor對(duì)象存在于每個(gè)對(duì)象的對(duì)象頭中,synchronized便是通過(guò)持有Monitor實(shí)現(xiàn)鎖的,這也是為什么 Java 中任意對(duì)象可以作為鎖的原因。

下面是從字節(jié)碼層面來(lái)分析synchronized關(guān)鍵字

public class SyncBlockAndMethod {
    public void syncTask() {
        //同步代碼塊
        synchronized (this) {
            System.out.println("Hello");
        }
    }

    //同步方法
    public void syncMethod() {
        System.out.println("Hello Again");

    }
}

在類中定義了同步代碼塊和同步方法,使用javac編譯為字節(jié)碼

javac SyncBlockAndMethod.java

然后使用javap查看編譯生成的字節(jié)碼

javap -verbose SyncBlockAndMethod

首先來(lái)看與synchronized修飾同步代碼塊有關(guān)的字節(jié)碼

  public void syncTask();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String Hello
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit
        ...
        ...
        ...

從字節(jié)碼中可知,synchronized同步語(yǔ)句塊的實(shí)現(xiàn)主要對(duì)應(yīng)于monitorentermonitorexit指令。monitorenter是同步代碼塊的入口,表示在這里獲取鎖。monitorexit是同步代碼塊的出口,表示在這里釋放鎖。

細(xì)心的同學(xué)可以看到字節(jié)碼中有一個(gè)monitorenter指令,卻有兩個(gè)monitorexit指令。第一個(gè)monitorexit指令是和monitorenter指令對(duì)應(yīng)的。

而為了保證在同步代碼塊中拋出異常時(shí)依然能夠釋放鎖,編譯器會(huì)自動(dòng)產(chǎn)生一個(gè)異常處理器,在同步代碼塊中拋出異常時(shí),會(huì)在這個(gè)異常處理器里面釋放鎖,對(duì)應(yīng)于字節(jié)碼中的第二個(gè)monitorexit指令。

下面來(lái)看與synchronized修飾方法有關(guān)的字節(jié)碼

  public synchronized void syncMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, 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 Again
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 13: 0
        line 15: 8

上面沒(méi)有看到monitorentermonitorexit指令,因?yàn)樵?code>synchronized修飾方法時(shí),monitor的實(shí)現(xiàn)是隱式的,在方法的flags中添加了ACC_SYNCHRONIZED標(biāo)志位,通過(guò)這種方式來(lái)實(shí)現(xiàn)synchronized方法。當(dāng)一個(gè)線程進(jìn)入synchronized方法時(shí)會(huì)獲取monitor對(duì)象,在方法體中拋出異?;蛘邎?zhí)行完成時(shí)會(huì)釋放monitor。

什么是重入:

從互斥鎖的設(shè)計(jì)上來(lái)說(shuō),當(dāng)一個(gè)線程視圖操作一個(gè)由其他線程持有的對(duì)象鎖的臨界資源時(shí),將會(huì)處于阻塞狀態(tài)。但是當(dāng)一個(gè)線程再次請(qǐng)求自己持有對(duì)象鎖的臨界資源時(shí),這種情況屬于重入,是可以請(qǐng)求成功的。
舉個(gè)例子:一個(gè)線程在獲得鎖進(jìn)入synchronized方法時(shí),在方法內(nèi)部又調(diào)用了該對(duì)象另一個(gè)synchronized方法,是可以調(diào)用成功的。

在早期版本中,synchronized屬于重量鎖,Monitor依賴于底層操作系統(tǒng)的Mutex Lock實(shí)現(xiàn)
線程獲得鎖之后會(huì)切換,而線程之間的切換需要用用戶態(tài)轉(zhuǎn)換到和心態(tài),開(kāi)銷較大
在 Java6 以后,在 JVM(Hospot) 層面對(duì)synchronized做了較大的優(yōu)化,synchronized性能得到了很大的提升,包括如下:

  • Adaptive Spinning(自適應(yīng)自旋鎖)
  • Lock Eliminate(鎖消除)
  • Lock Coarsening(鎖粗化)
  • Lightweight Locking(輕量級(jí)鎖)
  • Biased Locking(偏向鎖)
    這些優(yōu)化都是為了在線程之間更高效地共享數(shù)據(jù)以及解決競(jìng)爭(zhēng)問(wèn)題,從而提高程序的運(yùn)行效率

自旋鎖

在許多情況下,共享數(shù)據(jù)的鎖定狀態(tài)只會(huì)持續(xù)很短一段時(shí)間,為了這一點(diǎn)點(diǎn)的時(shí)間去切換線程并不值得,在多核 CPU 的情況下,完全可以讓另一個(gè)沒(méi)有獲取到鎖的線程通過(guò)執(zhí)行忙循環(huán)等待鎖的釋放,而不是切換線程讓出 CPU。這就是自旋鎖,在 Java4 就引入了,只是默認(rèn)是關(guān)閉的,到了 Java6 之后才變?yōu)槟J(rèn)開(kāi)啟自旋鎖。
如果其他線程占有鎖的時(shí)間非常短,那么自旋很快就能獲取到鎖,自旋鎖的性能會(huì)很好;但是如果鎖被其他線程長(zhǎng)期占有,那么性能上的開(kāi)銷就會(huì)比較大,因?yàn)樽孕褪茄h(huán),如果循環(huán)時(shí)間較長(zhǎng),那么就會(huì)白白消耗 CPU 的資源。因此在 自旋一定次數(shù)后仍然沒(méi)有獲取到鎖,那么就使用傳統(tǒng)的方式掛起并切換線程,可以使用preBlockSpin參數(shù)設(shè)置自旋次數(shù)。但是要確定不同場(chǎng)景下自旋次數(shù)是比較困難的,因此出現(xiàn)了自適應(yīng)自旋鎖

自適應(yīng)自旋鎖

在自適應(yīng)自旋鎖中,自旋的次數(shù)不再固定,而是由上一次在同一個(gè)鎖上的自旋時(shí)間以及鎖的擁有者的狀態(tài)來(lái)決定的。如果在一個(gè)鎖對(duì)象上,剛剛有一個(gè)線程通過(guò)自旋成功獲取到鎖,并且持有鎖的線程正在運(yùn)行黃總,JVM 會(huì)認(rèn)為通過(guò)自旋成功獲取到鎖的可能性很大,在下次其他線程獲取鎖時(shí)會(huì)增加自旋的次數(shù)。反之如果一個(gè)鎖很少可以通過(guò)自旋成功獲取,那么在之后獲取鎖時(shí),將跳過(guò)自旋過(guò)程,避免浪費(fèi)處理器資源。

鎖消除

鎖消除也是一種鎖優(yōu)化,在進(jìn)行 JIT 編譯時(shí),JVM 對(duì)運(yùn)行上下文進(jìn)行掃描,去除不可能存在競(jìng)爭(zhēng)的鎖,可以節(jié)省一些沒(méi)有意義的請(qǐng)求鎖的時(shí)間,提升程序的性能。在下面的例子中,StringBuffer 是線程安全的,append() 方法帶有synchronized關(guān)鍵字修飾,但是由于 sb 對(duì)象只會(huì)在add()方法內(nèi)被調(diào)用,屬于局部變量,不可能b被其他線程引用,因此 sb 對(duì)象屬于不可能共享的資源,JVM 會(huì)自動(dòng)消除 sb.append() 方法的鎖

public class StringBufferWithoutSync {
    public void add(String str1, String str2) {
        StringBuffer sb = new StringBuffer();
        //StringBuffer 是線程安全的,append() 方法帶有synchronized關(guān)鍵字修飾。
        //但是由于 sb 對(duì)象只會(huì)在add()方法內(nèi)被調(diào)用,屬于局部變量,不可能b被其他線程引用
        //因此 sb 對(duì)象屬于不可能共享的資源,JVM 會(huì)自動(dòng)消除 sb.append() 方法的鎖
        sb.append(str1).append(str2);
    }

    public static void main(String[] args) {
        StringBufferWithoutSync withoutSync = new StringBufferWithoutSync();
        for (int i = 0; i < 100; i++) {
            withoutSync.add("a", "b");
        }
    }
}

鎖粗化

通過(guò)擴(kuò)大加鎖的范圍,避免反復(fù)加鎖和釋放鎖

public class CoarseSync {
    public static String copyString100Times(String target) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < 100; i++) {
            //由于 append 方法是 synchronized 的。每次 append 都會(huì)去申請(qǐng)鎖,JVM 檢測(cè)到在循環(huán)中的加鎖和釋放鎖操作,
            // 比較耗時(shí),就會(huì)將鎖粗化到循環(huán)外部,只加一次鎖,這樣就提高了性能
            sb.append(target);
        }
        return sb.toString();
    }
}

在上面代碼中,由于 append 方法是 synchronized 的。每次 append 都會(huì)去申請(qǐng)鎖,JVM 檢測(cè)到在循環(huán)中的加鎖和釋放鎖操作,比較耗時(shí),就會(huì)將鎖粗化到循環(huán)外部,只加一次鎖,這樣就提高了性能。

synchronized 的四種狀態(tài)

無(wú)鎖、偏向鎖、輕量級(jí)鎖、重量級(jí)鎖。
會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí),鎖膨脹的方向?yàn)椋簾o(wú)鎖 -> 偏向鎖 -> 輕量級(jí)鎖 -> 重量級(jí)鎖。

在特定情況下還會(huì)進(jìn)行鎖降級(jí),當(dāng) JVM 運(yùn)行到安全點(diǎn)(Safe Point)時(shí),會(huì)檢查是否有空閑的Monitor,并嘗試將其進(jìn)行降級(jí)。

無(wú)鎖就對(duì)應(yīng)上面的鎖消除,重量鎖就是Monitor,下面重點(diǎn)說(shuō)一下偏向鎖和輕量級(jí)鎖。

偏向鎖的出現(xiàn)是為了減少同一線程獲取鎖的代價(jià)。
在大多數(shù)情況下,鎖不存在多線程競(jìng)爭(zhēng),總是由同一線程多次獲得。為了減少同一線程獲取鎖的代價(jià),引入了偏向鎖。

偏向鎖的核心思想是:如果一個(gè)線程獲得了鎖,那么鎖進(jìn)入偏向模式,此時(shí) Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu),當(dāng)該線程再次清秋鎖時(shí),無(wú)需再做任何同步操作,即獲取鎖的過(guò)程只需要檢查 Mark Word 的鎖標(biāo)記位,以及當(dāng)前線程 ID 等于 Mark Word 的 ThreadID即可,這樣就省去了大量有關(guān)鎖申請(qǐng)的操作。
當(dāng)一個(gè)線程訪問(wèn)同步塊并獲取到鎖時(shí),會(huì)在對(duì)象頭和棧幀中的鎖記錄里存儲(chǔ)鎖偏向的線程 ID,以后該線程再進(jìn)入和推出同步塊時(shí),不需要進(jìn)行 CAS 操作來(lái)加鎖和解鎖,從而提高了程序的性能。
對(duì)于沒(méi)有鎖競(jìng)爭(zhēng)的場(chǎng)合,偏向鎖有很好的優(yōu)化效果。但是對(duì)于鎖競(jìng)爭(zhēng)比較激烈的多線程場(chǎng)合,偏向鎖就失去作用了,這時(shí)偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖。

輕量級(jí)鎖

輕量級(jí)鎖是由偏向鎖升級(jí)來(lái)的,偏向鎖運(yùn)行在一個(gè)線程進(jìn)入同步塊的情況下,當(dāng)?shù)诙€(gè)線程加入鎖競(jìng)爭(zhēng)的時(shí)候,偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖。
輕量級(jí)鎖適用的場(chǎng)景是線程交替執(zhí)行同步塊的情況,若存在同一時(shí)間訪問(wèn)同一鎖的情況,就會(huì)導(dǎo)致輕量級(jí)鎖膨脹為重量級(jí)鎖。

  1. 在線程進(jìn)入同步代碼塊時(shí),如果同步對(duì)象鎖狀態(tài)為無(wú)鎖狀態(tài)(鎖標(biāo)志位為"01"狀態(tài)),虛擬機(jī)首先在當(dāng)當(dāng)前線程的棧幀中建立一個(gè)名為瑣記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的 Mark Word 的拷貝,官方稱之為 Displaced Mark Word,這時(shí)線程堆棧與對(duì)象頭的狀態(tài)如圖所示:
    <div align="center"> <img src="https://user-gold-cdn.xitu.io/2020/4/6/1714e3c8c16eb1b6?w=846&h=445&f=png&s=7226"/> </div>

  2. 把對(duì)象頭中的 Mark Word 拷貝到棧幀的瑣記錄中

  3. 拷貝成功后,虛擬機(jī)將使用 CAS 操作嘗試將對(duì)象的 Mark Word 更新為指向 Lock Record 的指針,并將 Lock Record 里的 owner 指針指向?qū)ο蟮?Mark Word,如果更次呢成功,則執(zhí)行步驟 4,否則執(zhí)行步驟 5

  4. 如果步驟 3 中的更新動(dòng)作成功了,那么這個(gè)線程就擁有了該對(duì)象的鎖,并且對(duì)象 Mark Word 的鎖標(biāo)志位設(shè)置為"00",表示該對(duì)象處于輕量鎖狀態(tài)。這時(shí)候線程棧幀與對(duì)象頭的狀態(tài)如圖所示:
    <div align="center"> <img src="https://user-gold-cdn.xitu.io/2020/4/6/1714e3c8c1c26102?w=849&h=477&f=png&s=27759"/> </div>

  5. 如果步驟 3 的更新操作失敗了,虛擬機(jī)會(huì)首先檢查對(duì)象的 Mark Word 的是否指向當(dāng)前線程的棧幀,如果時(shí)就說(shuō)明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,可以直接進(jìn)入同步塊繼續(xù)執(zhí)行。否則說(shuō)明多個(gè)線程在競(jìng)爭(zhēng)鎖,輕量級(jí)鎖膨脹為重量級(jí)鎖,鎖標(biāo)志的狀態(tài)變?yōu)?10",Mark Word 中存儲(chǔ)的就變?yōu)橹赶蛑亓考?jí)鎖的指針,當(dāng)前線程便嘗試使用自旋來(lái)獲取鎖,而后面等待鎖的線程要進(jìn)入阻塞狀態(tài)。

下面再來(lái)講講輕量級(jí)鎖解鎖的過(guò)程。

解鎖的過(guò)程:

  1. 通過(guò) CAS 操作嘗試把線程棧幀中復(fù)制的Displaced Mark Word 替換到對(duì)象當(dāng)前的 Mark Word 中
  2. 如果替換成功,整個(gè)同步過(guò)程就完成了
  3. 如果替換失敗,說(shuō)明由其他線程嘗試獲取該鎖(此時(shí)鎖已經(jīng)膨脹為重量級(jí)鎖),那就要在釋放鎖的同時(shí),喚醒被掛起的線程
    這里需要說(shuō)明一下為什么把棧幀中的 Displaced Mark Word 成功替換到對(duì)象的 Mark Word 中,就是解鎖成功了?這里需要從鎖的內(nèi)存語(yǔ)義來(lái)理解

鎖的內(nèi)存語(yǔ)義

  • 當(dāng)線程釋放鎖時(shí),Java 內(nèi)存模型會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中
  • 當(dāng)線程獲取鎖時(shí),Java 內(nèi)存模型會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無(wú)效,從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須從主內(nèi)存中讀取共享變量

也就是說(shuō)線程 A 釋放一個(gè)鎖,實(shí)際上是線程 A 向接下來(lái)將要獲取這個(gè)鎖的某個(gè)線程發(fā)出消息,這個(gè)消息就是線程 A 對(duì)共享變量所做的修改。
線程 B 獲取某個(gè)鎖,實(shí)際上是線程 B 接收之前占有這個(gè)鎖的線程發(fā)出的消息,這個(gè)消息就是在釋放鎖之前對(duì)共享變量所做的修改。

優(yōu)點(diǎn) 缺點(diǎn) 使用場(chǎng)景
偏向鎖 加鎖和解鎖不需要 CAS 操作,沒(méi)有額外的性能消耗,和執(zhí)行非同步方法相比僅存在納秒級(jí)的差別 如果線程之間存在鎖競(jìng)爭(zhēng),會(huì)帶來(lái)額外的鎖撤銷的消耗 只有一個(gè)線程訪問(wèn)同步代碼塊或者同步方法的場(chǎng)景
輕量級(jí)鎖 競(jìng)爭(zhēng)的線程不會(huì)阻塞,提高了相應(yīng)速度 如果線程長(zhǎng)時(shí)間搶占不到鎖,自旋會(huì)消耗 CPU 性能 線程交替執(zhí)行方法同步塊或者同步方法的場(chǎng)景
重量級(jí)鎖 線程競(jìng)爭(zhēng)不使用自旋,不會(huì)消耗 CPU 線程阻塞,響應(yīng)時(shí)間緩慢,在多線程下,頻繁地獲取和釋放鎖,會(huì)帶來(lái)巨大的性能消耗 追求吞吐量,同步代碼塊或者同步方法執(zhí)行時(shí)間較長(zhǎng)的場(chǎng)景


如果你覺(jué)得這篇文章對(duì)你有幫助,不妨點(diǎn)個(gè)贊,讓我有更多動(dòng)力寫(xiě)出好文章。

我的文章會(huì)首發(fā)在公眾號(hào)上,歡迎掃碼關(guān)注我的公眾號(hào)張賢同學(xué)。

公眾號(hào)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容