synchronized實(shí)現(xiàn)原理,以及JVM對鎖性能的優(yōu)化

線程安全,是Java并發(fā)編程中的重要關(guān)注點(diǎn),應(yīng)該注意到的是,造成線程安全問題的主要原因有兩點(diǎn):
1,存在共享數(shù)據(jù)(也稱臨界資源)
2,存在多條線程,共同操作共享數(shù)據(jù)。

本文由淺入深,逐步整理了synchronized的相關(guān)知識,主要包括:

  • 應(yīng)用場景
  • 原理概要
  • 原理詳解
  • 低層實(shí)現(xiàn)
  • 鎖的優(yōu)化(JDK1.6引入)
  • 鎖的升級(在什么情況下會升級,以及鎖只能單向升級)

應(yīng)用方式

synchronized 是解決Java并發(fā)最常見的一種方法,也是最簡單的一種方法。關(guān)鍵字 synchronized 可以保證在同一時刻,只有一個線程可以訪問某個方法或者某個代碼塊。同時 synchronized 也可以保證一個線程的變化,被另一個線程看到(保證了可見性)
這里要注意:synchronized是一個互斥的 重量級鎖 (細(xì)節(jié)部分后續(xù)會講)

synchronized的作用主要有三個:

  1. 確保線程互斥的訪問代碼
  2. 保證共享變量的修改能夠及時可見(可見性)
  3. 可以阻止JVM的指令重排序

在Java中所有對象都可以作為鎖,這是synchronized實(shí)現(xiàn)同步的基礎(chǔ)。
synchronized主要有三種應(yīng)用方式:

  1. 普通同步方法,鎖的是當(dāng)前實(shí)例的對象
  2. 靜態(tài)同步方法,鎖的是靜態(tài)方法所在的類對象
  3. 同步代碼塊,鎖的是括號里的對象。(此處的可以是實(shí)例對象,也可以是類的class對象。)

原理概要

Java虛擬機(jī)中的同步(Synchronization)都是基于進(jìn)入和退出Monitor對象實(shí)現(xiàn),無論是顯示同步(同步代碼塊)還是隱式同步(同步方法)都是如此。

  • 同步代碼塊
    monitorenter指令插入到同步代碼塊的開始位置。monitorexit指令插入到同步代碼塊結(jié)束的位置。JVM需要保證每一個monitorenter都有一個monitorexit與之對應(yīng)。
    任何對象,都有一個monitor與之相關(guān)聯(lián),當(dāng)monitor被持有以后,它將處于鎖定狀態(tài)。線程執(zhí)行到monitorenter指令時,會嘗試獲得monitor對象的所有權(quán),即嘗試獲取鎖。

虛擬機(jī)規(guī)范對 monitorenter 和 monitorexit 的行為描述中,有兩點(diǎn)需要注意。首先 synchronized 同步快對于同一條線程來說是可重入的,也就是說,不會出現(xiàn)把自己鎖死的問題。其次,同步快在已進(jìn)入的線程執(zhí)行完之前,會阻塞后面其他線程的進(jìn)入。(摘自《深入理解JAVA虛擬機(jī)》)

  • 同步方法
    synchronized方法則會被翻譯成普通的方法調(diào)用和返回指令如:invokevirtual、areturn指令,在VM字節(jié)碼層面并沒有任何特別的指令來實(shí)現(xiàn)被synchronized修飾的方法,而是在Class文件的方法表中將該方法的access_flags字段中的synchronized標(biāo)志位置1,表示該方法是同步方法并使用調(diào)用該方法的對象或該方法所屬的Class在JVM的內(nèi)部對象表示Klass做為鎖對象。

原理詳解

要理解低層實(shí)現(xiàn),就需要理解兩個重要的概念 MonitorMark Word

  • Java對象頭

synchronized用到的鎖,是存儲在對象頭中的。(這也是Java所有對象都可以上鎖的根本原因)
HotSpot虛擬機(jī)中,對象頭包括兩部分信息:
Mark Word(對象頭)和 Klass Pointer(類型指針)

  • 其中類型指針,是對象指向它的類元素的指針,虛擬機(jī)通過這個指針來確定這個對象是哪個類的實(shí)例。
  • 對象頭又分為兩部分:第一部分存儲對象自身的運(yùn)行時數(shù)據(jù),例如哈希碼,GC分代年齡,線程持有的鎖,偏向時間戳等。這一部分的長度是不固定的。第二部分是末尾兩位,存儲鎖標(biāo)志位,表示當(dāng)前鎖的級別。

對象頭的長度一般占用兩個機(jī)器碼(32位JVM中,一個機(jī)器碼等于4個字節(jié),也就是32bit),但如果對象是數(shù)組類型,則需要三個機(jī)器碼(多出的一塊記錄數(shù)組長度)。

下圖是對象頭運(yùn)行時的變化狀態(tài)
鎖標(biāo)志位是否偏向鎖 確定唯一的鎖狀態(tài)
其中 輕量鎖 和 偏向鎖 是JDK1.6之后新加的,用于對synchronized優(yōu)化。稍后講到

java對象頭

  • Monitor

Monitor是 synchronized 重量級 鎖的實(shí)現(xiàn)關(guān)鍵。鎖的標(biāo)識位為 10 。當(dāng)然 synchronized作為一個重量鎖是非常消耗性能的,所以在JDK1.6以后做了部分優(yōu)化,接下來的部分是講作為重量鎖的實(shí)現(xiàn)。

Monitor是線程私有的數(shù)據(jù)結(jié)構(gòu),每一個對象都有一個monitor與之關(guān)聯(lián)。每一個線程都有一個可用monitor record列表(當(dāng)前線程中所有對象的monitor),同時還有一個全局可用列表(全局對象monitor)。每一個被鎖住的對象,都會和一個monitor關(guān)聯(lián)。

當(dāng)一個monitor被某個線程持有后,它便處于鎖定狀態(tài)。此時,對象頭中 MarkWord的 指向互斥量的指針,就是指向鎖對象的monitor起始地址。
monitor是由 ObjectMonitor 實(shí)現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下:(位于HotSpot虛擬機(jī)源碼ObjectMonitor.hpp文件,C++實(shí)現(xiàn)的)

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //記錄個數(shù)
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //處于wait狀態(tài)的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //處于等待鎖block狀態(tài)的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

object monitor 有兩個隊列 _EntryList_WaitSet ,用來保存ObjectWaiter對象列表(每個等待鎖的線程都會被封裝成ObjectWaiter對象)_owner 指向持有 objectMonitor的線程。

當(dāng)多個線程同時訪問一個同步代碼時,首先會進(jìn)入 _EntryList 集合,當(dāng)線程獲取到對象的monitor后,會進(jìn)入_owner 區(qū)域,然后把monitor中的 _owner 變量修改為當(dāng)前線程,同時monitor中的計數(shù)器_count 會加1。

根據(jù)虛擬機(jī)規(guī)范的要求,在執(zhí)行monitorenter指令時,會嘗試獲取對象的鎖。如果對象沒有被鎖定(獲取鎖),獲取對象已經(jīng)被該線程鎖定(鎖重入)。則把計數(shù)器加1(_count 加1)。相應(yīng)的,在執(zhí)行monitorexit指令時,會講計數(shù)器減1。當(dāng)計數(shù)器為0時,_owner指向Null,鎖就被釋放。(摘自《深入理解JAVA虛擬機(jī)》)

如果線程調(diào)用 wait() 方法,將釋放當(dāng)前持有的monitor,_owner變量恢復(fù)為null,_count變量減1,同時該線程進(jìn)入_WaitSet 等待被喚醒。


底層實(shí)現(xiàn)

  • synchronized 代碼塊低層原理

從Javac編譯成的字節(jié)碼可以看出(具體編譯文件看參考鏈接),同步代碼塊使用的是monitorentermonitorexit指令,其中monitorenter指向同步代碼塊的開始位置,monitorexit指向同步代碼塊的結(jié)束位置。

在線程執(zhí)行到monitorenter指令時,當(dāng)前線程將嘗試獲取鎖,即嘗試獲取鎖對象對應(yīng)的monitor的持有權(quán)。當(dāng)monitor的count計數(shù)器為0,或者monitor的owner已經(jīng)是該線程,則獲取鎖,count計數(shù)器+1。
如果其他線程已經(jīng)持有該對象的鎖,則該線程被阻塞,直到其他線程執(zhí)行完畢釋放鎖。

線程執(zhí)行完畢時,count歸零,owner指向Null,鎖釋放。

值得注意的是,編譯器將會確保,無論通過何種方法完成,方法中的每一條monitorenter指令,最終都會有monitorexit指令對應(yīng),不論這個方法正常結(jié)束還是異常結(jié)束,最終都會配對執(zhí)行。
編譯器會自動產(chǎn)生一個異常處理器,這個處理器聲明可以處理所有的異常,它的目的就是為了確保monitorexit指令最終執(zhí)行。


  • synchronized 方法低層原理

方法級的同步是隱式,即無需通過字節(jié)碼來控制的,它實(shí)現(xiàn)在方法調(diào)用和返回操作中。
在Class文件方法常量池中的方法表結(jié)構(gòu)(method_info Structure)中, ACC_SYNCHRONIZED 訪問標(biāo)志區(qū)分一個方法是否為同步方法。在方法被調(diào)用時,會檢查方法的 ACC_SYNCHRONIZED 標(biāo)記是否被設(shè)置。如果被設(shè)置了,則線程將持有該方法對應(yīng)對象的monitor(調(diào)用方法的實(shí)例對象or靜態(tài)方法的類對象),然后再執(zhí)行該方法。
最后在方法執(zhí)行完成時,釋放monitor。
在方法執(zhí)行期間,執(zhí)行線程持有了monitor,其他任何線程都無法再獲得同一個monitor。
以下是字節(jié)碼實(shí)現(xiàn):

public class SyncMethod {

   public int i;

   public synchronized void syncTask(){
           i++;
   }
}

使用javap反編譯后的字節(jié)碼如下:

Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncMethod.class
  Last modified 2017-6-2; size 308 bytes
  MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
  Compiled from "SyncMethod.java"
public class com.zejian.concurrencys.SyncMethod
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool;

   //省略沒必要的字節(jié)碼
  //==================syncTask方法======================
  public synchronized void syncTask();
    descriptor: ()V
    //方法標(biāo)識ACC_PUBLIC代表public修飾,ACC_SYNCHRONIZED指明該方法為同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
}
SourceFile: "SyncMethod.java"

從字節(jié)碼可以看出,synchronized修飾的方法并沒有monitorentermonitorexit指令。而是用ACC_SYNCHRONIZED的flag標(biāo)記該方法是否是同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用。


鎖的狀態(tài)和優(yōu)化

在早期的Java版本中,synchronized屬于重量級鎖,效率低下,因為監(jiān)視器鎖(Monitor)是依賴于低層的操作系統(tǒng)的Mutex Lock來實(shí)現(xiàn)的。
而操作系統(tǒng)實(shí)現(xiàn)線程中的切換時,需要用用戶態(tài)切換到核心態(tài),這是一個非常重的操作,時間成本較高。這也是早期 synchronized 效率低下的原因。

JDK1.6之后JVM官方對鎖做了較大優(yōu)化:
引入了:

  • 鎖粗化(Lock Coarsening)
  • 鎖消除(Lock Elimination)
  • 適應(yīng)性自旋(Adaptive Spinning)

同時增加了兩種鎖的狀態(tài):

  • 偏向鎖(Biased Locking)
  • 輕量鎖(Lightweight Locking)
先說鎖的狀態(tài):

鎖的狀態(tài)共有四種:無鎖,偏向鎖,輕量鎖,重量鎖。隨著鎖的競爭,鎖會從偏向鎖升級為輕量鎖,然后升級為重量鎖。鎖的升級是單向的,JDK1.6中默認(rèn)開啟偏向鎖和輕量鎖。

  • 偏向鎖

引入偏向鎖的目的是:為了在無多線程競爭的情況下,盡量減少不必要的輕量鎖執(zhí)行路徑。
因為經(jīng)過研究發(fā)現(xiàn),在大部分情況下,鎖并不存在多線程競爭,而且總是由一個線程多次獲得鎖。因此為了減少同一線程獲取鎖(會涉及到一些耗時的CAS操作)的代價而引入。
如果一個線程獲取到了鎖,那么該鎖就進(jìn)入偏向鎖模式,當(dāng)這個線程再次請求鎖時無需做任何同步操作,直接獲取到鎖。這樣就省去了大量有關(guān)鎖申請的操作,提升了程序性能。

獲取偏向鎖

  1. 檢查Mark Word 是否為可偏向狀態(tài),即是否為偏向鎖=1,鎖標(biāo)志位=01.
  2. 若為可偏向狀態(tài),則檢查 線程ID 是否為當(dāng)前對象頭中的線程ID,如果是,則獲取鎖,執(zhí)行同步代碼塊。如果不是,進(jìn)入第3步。
  3. 如果線程ID不是當(dāng)前線程ID,則通過CAS操作競爭鎖,如果競爭成功。則將Mark Word中的線程ID替換為當(dāng)前線程ID,獲取鎖,執(zhí)行同步代碼塊。如果沒成功,進(jìn)入第4步。
  4. 通過CAS競爭失敗,則說明當(dāng)前存在鎖競爭。當(dāng)執(zhí)行到達(dá)全局安全點(diǎn)時,獲得偏向鎖的進(jìn)程會被掛起,偏向鎖膨脹為輕量級鎖(重要),被阻塞在安全點(diǎn)的線程繼續(xù)往下執(zhí)行同步代碼塊。

釋放偏向鎖
偏向鎖的釋放,采取了一種只有競爭才會釋放鎖的機(jī)制,線程不會主動去釋放鎖,需要等待其他線程來競爭。偏向鎖的撤銷需要等到全局安全點(diǎn)(這個時間點(diǎn)沒有正在執(zhí)行的代碼),步驟如下:

  1. 暫停擁有偏向鎖的線程,判斷對象是否還處于被鎖定的狀態(tài)。
  2. 撤銷偏向鎖。恢復(fù)到無鎖狀態(tài)(01)或者 膨脹為輕量級鎖。
    偏向鎖的獲取和釋放流程


  • 輕量級鎖

輕量鎖能夠提升性能的依據(jù),是基于如下假設(shè):即在真實(shí)情況下,程序中的大部分代碼一般都處于一種無鎖競爭的狀態(tài)(即單線程環(huán)境),而在無鎖競爭下完全可以避免調(diào)用操作系統(tǒng)層面的操作來實(shí)現(xiàn)重量鎖。如果打破這個依據(jù),除了互斥的開銷外,還有額外的CAS操作,因此在有線程競爭的情況下,輕量鎖比重量鎖更慢。
為了減少傳統(tǒng)重量鎖造成的性能不必要的消耗,才引入了輕量鎖。

當(dāng)關(guān)閉偏向鎖功能 或者 多個線程競爭偏向鎖導(dǎo)致升級為輕量鎖,則會嘗試獲取輕量鎖。

獲取輕量鎖

  1. 判斷當(dāng)前對象是否處于無鎖狀態(tài)(偏向鎖標(biāo)記=0,無鎖狀態(tài)=01),如果是,則JVM會首先將當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲當(dāng)前對象的Mark Word拷貝。(官方稱為Displaced Mark Word)。接下來執(zhí)行第2步。如果對象處于有鎖狀態(tài),則執(zhí)行第3步
  2. JVM利用CAS操作,嘗試將對象的Mark Word更新為指向Lock Record的指針。如果成功,則表示競爭到鎖。將鎖標(biāo)志位變?yōu)?0(表示此對象處于輕量級鎖的狀態(tài)),執(zhí)行同步代碼塊。如果CAS操作失敗,則執(zhí)行第3步。
  3. 判斷當(dāng)前對象的Mark Word 是否指向當(dāng)前線程的棧幀,如果是,則表示當(dāng)前線程已經(jīng)持有當(dāng)前對象的鎖,直接執(zhí)行同步代碼塊。否則,說明該鎖對象已經(jīng)被其他對象搶占,此后為了不讓線程阻塞,還會進(jìn)入一個自旋鎖的狀態(tài),如在一定的自旋周期內(nèi)嘗試重新獲取鎖,如果自旋失敗,則輕量鎖需要膨脹為重量鎖(重點(diǎn)),鎖標(biāo)志位變?yōu)?0,后面等待的線程將會進(jìn)入阻塞狀態(tài)。

釋放輕量鎖
輕量級鎖的釋放操作,也是通過CAS操作來執(zhí)行的,步驟如下:

  1. 取出在獲取輕量級鎖時,存儲在棧幀中的 Displaced Mard Word 數(shù)據(jù)。
  2. 用CAS操作,將取出的數(shù)據(jù)替換到對象的Mark Word中,如果成功,則說明釋放鎖成功,如果失敗,則執(zhí)行第3步。
  3. 如果CAS操作失敗,說明有其他線程在嘗試獲取該鎖,則要在釋放鎖的同時喚醒被掛起的線程。


    輕量鎖的獲取和釋放


  • 重量級鎖

重量級鎖通過對象內(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īng)性自旋

在輕量級鎖獲取失敗時,為了避免線程真實(shí)的在系統(tǒng)層面被掛起,還會進(jìn)行一項稱為自旋鎖的優(yōu)化手段。

這是基于以下假設(shè):
大多數(shù)情況下,線程持有鎖的時間不會太長,將線程掛起在系統(tǒng)層面耗費(fèi)的成本較高。
而“適應(yīng)性”則表示,該自學(xué)的周期更加聰明。自旋的周期是不固定的,它是由上一次在同一個鎖上的自旋時間 以及 鎖擁有者的狀態(tài) 共同決定。

具體方式是:如果自旋成功了,那么下次的自旋最大次數(shù)會更多,因為JVM認(rèn)為既然上次成功了,那么這一次也有很大概率會成功,那么允許等待的最大自旋時間也相應(yīng)增加。反之,如果對于某一個鎖,很少有自旋成功的,那么就會相應(yīng)的減少下次自旋時間,或者干脆放棄自旋,直接升級為重量鎖,以免浪費(fèi)系統(tǒng)資源。

有了適應(yīng)性自旋,隨著程序的運(yùn)行信息不斷完善,JVM會對鎖的狀態(tài)預(yù)測更加精準(zhǔn),虛擬機(jī)會變得越來越聰明。


再談?wù)勬i的優(yōu)化:


  • 鎖粗化

我們知道,在使用鎖的時候,需要讓同步的作用范圍盡可能的小——僅在共享數(shù)據(jù)的操作中才進(jìn)行。這樣做的目的,是為了讓同步操作的數(shù)量盡可能小,如果村子鎖競爭,那么也能盡快的拿到鎖。
在大多數(shù)的情況下,上面的原則是正確的。
但是如果存在一系列連續(xù)的 lock unlock 操作,也會導(dǎo)致性能的不必要消耗.
粗化鎖就是將連續(xù)的同步操作連在一起,粗化為一個范圍更大的鎖。
例如,對Vector的循環(huán)add操作,每次add都需要加鎖,那么JVM會檢測到這一系列操作,然后將鎖移到循環(huán)外。


  • 鎖消除

鎖消除是JVM進(jìn)行的另外一項鎖優(yōu)化,該優(yōu)化更徹底。

JVM在進(jìn)行JIT編譯時,通過對上下文的掃描,JVM檢測到不可能存在共享數(shù)據(jù)的競爭,如果這些資源有鎖,那么會消除這些資源的鎖。這樣可以節(jié)省毫無意義的鎖請求時間。

雖然大部分程序員可以判斷哪些操作是單線程的不必要加鎖,但我們在使用Java的內(nèi)置 API時,部分操作會隱性的包含鎖操作。例如StringBuffer的操作,HashTable的操作。

鎖消除的依據(jù),是逃逸分析的數(shù)據(jù)支持。



(如果有什么錯誤或者建議,歡迎留言指出)
(本文內(nèi)容是對各個知識點(diǎn)的轉(zhuǎn)載整理,用于個人技術(shù)沉淀,以及大家學(xué)習(xí)交流用)


參考資料:
**【死磕Java并發(fā)】深入分析synchronized實(shí)現(xiàn)原理
** 深入理解Java并發(fā)之synchronized原理
JVM內(nèi)部細(xì)節(jié)之synchronized實(shí)現(xiàn)細(xì)節(jié)
Java對象頭解析-不得不了解的對象頭

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

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

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