《深入瞭解java虛擬機器器》高效並行讀書筆記——Java記憶體模型,執行緒,執行緒安全 與鎖優化

2022-07-31 12:00:09

《深入瞭解java虛擬機器器》高效並行讀書筆記——Java記憶體模型,執行緒,執行緒安全 與鎖優化

本文主要參考《深入瞭解java虛擬機器器》高效並行章節
關於鎖升級,偏向鎖,輕量級鎖參考《Java並行程式設計的藝術》
關於執行緒安全和執行緒安全的程度參考了《Java並行程式設計實戰》
圖片參考https://www.processon.com/u/5dee0443e4b093b9f775065c#pc

一丶Java記憶體模型

1.概述

多工處理已經是作業系統的必備技能,計算機被要求同時做好幾件事情,不僅是由於計算機計算能力強大了,還因為cpu的計算能力和儲存以及通訊子系統的速度差異太大了(指cpu工作的時候大部分時間花費在網路io,磁碟io上)所以人們開始讓處理器同時進行多個任務而不是浪費時間等待io操作的完成,從而提高TPS(每秒事務處理數)

2.硬體的效率和快取一致性

  • 解決cpu和記憶體運算能力差距巨大的問題

    為了解決CPU和儲存子系統的存在幾個數量級的問題,現代計算機系統引入了多層的高速緩衝作為記憶體和處理器之間的緩衝(有點類似於使用redis提高系統的存取速度,當我們系統的TPS受限於資料的io時,常常把熱點資料放在基於記憶體的redis從而提高TPS)把運算需要資料複製到快取,提高運算速度,當運算結束後再從快取同步到記憶體,從而使處理器不必等待緩慢的記憶體讀寫了

  • 快取記憶體引入的新問題

    快取記憶體的引入確實很好的解決了處理器和記憶體速度的問題,但是同時也引入了新的問題——快取一致性:多處理器系統中,每個處理器擁有自己的快取記憶體且共用主記憶體,當多個處理器的運算涉及到同一主記憶體的時候,可能存在快取不一致的問題(ABC三個處理器,最開始快取資料為1,後續各自分別自加1,2,3,最後需要寫回主記憶體,這時候出現快取不一致的問題)

  • 指令重排

    為了使處理器內部運算單元可以被充分利用,處理器可能會對輸入的程式碼進行亂序執行的優化,處理器在計算之後把亂序執行結果重組,保證結果和程式設計師定義的程式碼順序執行結果相同,但是並不保證程式中每一個語句的計算先後順序與輸入程式碼中順序一致。所以如果存在一個計算任務依賴另一個任務的中間結果的時候將無法依靠程式碼的順序來進行保證,Java虛擬機器器的即時編譯器也存在這樣的指令重排技術

3.Java記憶體模型

遮蔽了作業系統的差異,保證java程式碼在不同的作業系統上並行完全正常,Java記憶體模型簡稱為JMM

3.1主記憶體和工作記憶體

JMM定義了程式中共用變數的存取規則,關注於虛擬機器器把共用變數儲存到記憶體和從記憶體取值的底層細節。(共用變數指的是執行緒共用的欄位,常包括範例欄位,靜態欄位,構成數物件的元素,但是不包括區域性變數和方法引數,這裡的區域性變數是reference型別,雖然它指向的物件在堆中是執行緒共用的但是reference本身是在棧的區域性變數中,是執行緒私有的)。 JMM規定了:

  1. 所有變數儲存在主記憶體,每條執行緒還有自己的工作記憶體(類似快取記憶體)工作記憶體儲存該執行緒使用變數的主記憶體的副本(不一定是複製整個物件,也許是這個物件被使用的某些欄位,即使是volatile變數也存線上程工作記憶體的副本),
  2. 執行緒對變數的操作必須在工作記憶體中進行,而不能直接操作主記憶體
  3. 不同執行緒不能直接存取對方工作記憶體中的變數,執行緒中的變數傳遞必須通過主記憶體來完成

3.2記憶體間互動操作

3.2.1 八種記憶體互動的操作

JMM規定了一個變數如何從主記憶體拷貝到工作記憶體,如何從工作記憶體同步到主記憶體的實現細節,並且定義了8中類似的操作,Java虛擬機器器保證以下操作都是原子的不可再分的(這裡64位元的double型別,long型別在某寫平臺可能load,store,read,write並不保證原子性,也就意味著可能存在半讀半寫的情況,但是無需過於關注這一點)

  • lock:作用於主記憶體的變數,把變數標識位執行緒獨佔的狀態
  • unlock: 作用於主記憶體變數,表示把處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒lock
  • read: 從主記憶體讀取變數傳輸到執行緒工作記憶體,以便後續load操作
  • load: 作用於工作記憶體變數,把read讀取到主記憶體的變數放入工作記憶體的變數副本中
  • use:作用於工作記憶體變數,表示把工作記憶體中的變數傳遞給執行引擎
  • assign: 作用於工作記憶體的變數,表示從執行引擎接收到的值賦值給工作記憶體中的變數
  • store:作用於工作記憶體中的變數,表示把工作記憶體中的變數傳送到主記憶體中,以便後續write操作
  • write: 作用主記憶體的變數,把store操作從工作記憶體中得到的值放入到主記憶體的變數
JMM要求一個變數從主記憶體拷貝工作記憶體,必須順序的執行read load
要把變數從工作記憶體同步到主記憶體,也要順序執行store write
但是兩個命令之間可以插入其他命令
到這裡我們可以簡單分析下為什麼i++這種操作執行緒不安全
首先i一開始位於主記憶體,被多個執行緒使用read 和load 從主記憶體複製到工作記憶體,
然後每一個執行緒使用use進行自增操作,
然後assign 賦值給執行緒私有的工作記憶體,
然後每一個執行緒使用store把工作記憶體中值傳送到主記憶體,
然後使用write操作寫到主記憶體

雖然這每一步都是原子性的,但是合在一起就不是原子性的,
也就是說執行緒A write到主記憶體的時候,執行緒B也進行了write 造成都寫入了2,
執行緒C也沒有線上程A寫回主記憶體後再拉去i的值進行自增,
依舊使用的是工作記憶體中的i
3.2.2 八種基本操作必須滿足的規則
  • read和 load,store 和write 不可單獨操作,也就是是從主記憶體讀取變數到工作記憶體後續筆記存在load操作讓工作儲存變數的副本,同樣從工作記憶體傳送到主記憶體後續也必須存在write操作讓主記憶體寫入工作記憶體中的值到主記憶體。
  • 不允許執行緒丟棄最近的assign操作,也就是說工作記憶體中改變了變數,必須要同步回主記憶體
  • 不允許一個執行緒無任何原因(沒有發生assign操作)把資料從工作記憶體同步回主記憶體
  • 一個變數只可以從主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化的(load 或者assign)的變數,也就是對一個變數use 和store操作之前必須先執行assign操作和load操作
  • 一個變數在同一個時刻只允許一個執行緒對其進行lock操作,但是lock操作可以被一個執行緒執行多次,但是lock多少次也需要unlock多少次
  • 如果對一個變數指向了lock操作,那麼會清空工作記憶體中的此變數值,執行引擎使用這個變數之前,必須重新執行load 或assign來初始化變數的值
  • 如果一個變數沒有被lock 那麼也不能被unlock,A執行緒不能unlock B執行緒lock的變數
  • 對一個變數指向unlock之前,必須把這個變數同步會主記憶體中(store然後write)

3.1 volatile

3.1.1 volatile 的特性

volatile是java提供的輕量級同步機制,當一個變數被定義成volatile後,它會具備以下兩個特性

  • 對所有執行緒的可見性,即任何一個執行緒修改了volatile修飾的變數,對於其他執行緒都是可以立即得知(普通變數一個執行緒修改之後必須同步會主記憶體,然後另外一個執行緒從主記憶體重新讀取才可以得知變化)
  • 禁止指令重排序優化,普通的變數只能保證該方法執行的時候所有依賴賦值結果的地方都能正確的結果,表現為"執行緒內表現為序列的語意"

3.1.2 執行緒可見性和禁止指令重排是如何實現的

  • 執行緒可見性

    volatile修飾的變數,在賦值後會多執行一個lock add$0xx,(%esp) 的操作,lock字首的指令會導致將當前工作記憶體此變數寫回到主記憶體寫回記憶體的操作會是其他CPU中的快取了此變數的資料無效,每個處理通過嗅探匯流排上傳播的資料來判斷自己的快取資料是否過期,如果過期當處理器需要對資料進行修改的時候將從主記憶體中獲取最新的資料

  • 禁止指令重排序

    lock add$0xx,(%esp) 把修改同步到主記憶體意味著前面指令的操作以及執行完成,也就是說lock之前的指令不可在lock指令之後,從而實現重排序無法越過記憶體屏障的效果

    這裡我們說下為什麼雙重if+synchronizated為什麼要把參照使用volatile修飾
    建立一個物件範例,可以分為三步:
    1.分配物件記憶體。
    2.呼叫構造器方法,執行初始化。
    3.將物件參照賦值給變數。
    
    如果不適應volatile 可能出現被重排為
    1.分配物件記憶體。
    2.將物件參照賦值給變數。
    3.呼叫構造器方法,執行初始化。
    
    假設這個執行緒A正在執行new Instance()下執行倒2,
    已經改變了instance的指向
    這個時候執行緒B也呼叫getInstance 將導致執行緒B獲得還沒有執行初始化的物件範例
    
    volatile在雙重if+synchronizated的單例實際是起到了禁止指令重排的作用
    插入`lock add$0xx,(%esp)`保證了1,2,3都完成執行完,1,2,3可以進行重排,但是1,2,3必須都在`lock add$0xx,(%esp)`的前面執行
    

4 先行發生原則

4.1先行發生原則是什麼:

先行發生原子是我們判斷資料是否存在競爭,執行緒是否安全的有用手段。先行發生是java記憶體模型中定義兩項操作之前的偏序關係,並不意味著時間上的先後,而是說先行發生的操作對後續操作是可見的,比如我們說A先行發生於B,那麼意味著A操作造成的影響對B是可見的

4.2 Java記憶體模型下的先行發生原則

  • 程式次序規則:指一個執行緒內,按照控制六順序,書寫在前面的操作必然是對書寫在後面的操作可見的
  • 管程鎖定規則:unlock操作先行發生於後面對同一個鎖的lock操作(後面是時間上的先後)
  • volatile 變數規則:同一個volatile變數的寫先行發生於後面對此變數的讀(後面是時間上的先後)
  • 執行緒啟動規則:Thread的start方法先行發生於此執行緒的任何動作
  • 執行緒終止規則:執行緒中所有操作都先行發生於對此執行緒的終止檢測,所以Thread::isAlive 可以檢查當前執行緒是否終止
  • 物件終結規則: 一個執行緒的初始化完成先行發生於finalize方法的開始
  • 傳遞性:A先行發生於B,B先行發生於C那麼A先行發生於C

4.3先行發生原則的使用

private int value;
public void setValue(int value){
	this.value=value;
}
public int getValue(){
    return value;
}

如果執行緒A在10:20:01 呼叫了setValue(1),執行緒B在10:20:02呼叫了getValue()那麼執行緒B得到的是什麼暱,顯然是無法確定的也許是0,也許是1,因為無法確定執行緒A重新整理工作記憶體到主記憶體和執行緒B重新獲取主記憶體中值的先後順序。

  1. 使用volatile修飾value

    套用volatile 變數規則:同一個volatile變數的寫先行發生於後面對此變數的讀,所以執行緒A寫入是先於執行緒B的讀取,所以B可以正確拿到值

  2. 使用synchronized修飾方法

    套用管程鎖定規則:unlock操作先行發生於後面對同一個鎖的lock操作(後面是時間上的先後),A釋放鎖在B拿到鎖之前,B也可以成功拿到值

同樣我們還可以使用基於AQS實現的ReentrantLock(見我的筆記《JUC原始碼學習筆記1——AQS獨佔模式和ReentrantLock》),我的理解是ReentrantLock套用的是volatile 變數規則,加鎖解鎖其實是修改state這個volatile變數,然後volatile可以防止指令重排,對value的設定肯定是位於鎖獲取和釋放之間的

二丶Java與執行緒

1.java執行緒的實現

執行緒是比程序更輕量級的排程執行單位,執行緒的引入,可以把一個程序的資源分配和執行排程分開,各個執行緒可以共用程序資源(記憶體地址,檔案io)又可以獨立排程。對於hotspot虛擬機器器來說,Java執行緒都是對映到一個作業系統原生執行緒來實現,而且中間沒有額外的間接結構,所以hotspot不會干預執行緒排程的(只能通過執行緒優先順序給作業系統排程的建議),何時凍結執行緒,給執行緒分配多少處理器執行時間,執行緒安排給哪個處理器核心執行都是由作業系統完成的且全權決定的。

2.java執行緒的排程

執行緒的排程是指系統為執行緒分配處理器使用權的過程,排程方式主要有兩種:協同式排程(使用多少時間由執行緒本身控制)搶佔式——每個執行緒由系統來分配執行時間,執行緒的切換不由執行緒本身決定,但是執行緒可以主動讓出執行時間比如Thread::yield()方法,但是想主動獲取處理器執行時間,執行緒本身無法做到,搶佔式排程避免了協同式排程執行緒處理器執行時間不可控制的問題

三丶執行緒安全

1.執行緒安全的概念

當多個執行緒同時存取一個物件時,如果不用考慮這些多執行緒在執行時環境下的排程和交替執行,也不需要進行額外的同步,或者呼叫方不需要進行任何其他的協調工作,呼叫這個物件的行為都可以獲取正確的結構,那麼我們稱這個物件時執行緒安全的

執行緒安全的物件封裝了必要的正確性保障手段,呼叫方無須過多擔心多執行緒問題

2.執行緒安全程度

2.1.不可變

不可變物件一定時執行緒安全的,無論時方法的實現或者還是方法的呼叫者,都不需要過多的關注執行緒安全問題,比如說String(拋開反射修改char陣列的奇葩行為)大部分方法都是生產一個新的String物件,以及Integer等。在java中,如果一個多執行緒共用的資料是一個final修飾的基本型別,那麼必定不會再這個基本型別上存線上程安全問題,但是如果是一個參照型別那麼需要我們自己自行保證執行緒安全。

2.2 絕對執行緒安全

不管執行時環境如何,呼叫者都不需要額外的同步措施的多線是絕對執行緒安全的物件。這個絕對是比較難以理解的,比如Vector使用synchronized修飾方法,那麼就是執行緒絕對安全的麼?不是我們只能保證單獨的操作是執行緒安全的,但是比如先contain檢視是否包含然後add這種複合操作任然需要我們自己加鎖保證。這裡的絕對是說無論我如何使用當前這個類那麼都是執行緒安全的

2.3相對執行緒安全

這種就是我們常常說的執行緒安全,只需要保證這種物件單次的操作是執行緒安全的,不需要任何額外手段保證執行緒安全,但是一些連續的符合方法呼叫任然需要自行保證執行緒安全

2.4 執行緒相容

執行緒相容是物件本身不是執行緒安全的,但是可以通過在呼叫端使用一些手段保證執行緒安全,那麼就可以保證這個物件在並行中是安全的。這就是我們常說的執行緒不安全

2.5 執行緒對立

不論呼叫端是否採取同步措施,都無法保證執行緒安全的物件。比如Thread的suspend(),resume,如果兩個執行緒分別呼叫另外一個執行緒的這兩個方法,都存在死鎖的風險。

當執行緒A使用鎖L,如果執行緒A處於suspend狀態,而另一個執行緒B,需要L這把鎖才能resume執行緒A,因為執行緒A處理suspend狀態,是不會釋放L鎖,所以執行緒B線獲取不到這個鎖,而一直處於爭奪L鎖的狀態,最終導致死鎖。

3.如何實現執行緒安全

1.互斥同步

最常見的執行緒安全手段,同步的意思時多個執行緒並行存取共用資料時,保證共用資料同一個時刻只能被一個或一些執行緒存取到。互斥是實現同步的一種方式,臨界區,互斥量,號誌是常見的互斥方式。

臨界區:保證在某一時刻只有一個執行緒能存取資料的簡便辦法。在任意時刻只允許一個執行緒對共用資源進行存取。如果有多個執行緒試圖同時存取臨界區,那麼 在有一個執行緒進入後其他所有試圖存取此臨界區的執行緒將被掛起,並一直持續到進入臨界區的執行緒離開。

互斥量:互斥量跟臨界區很相似,只有擁有互斥物件的執行緒才具有存取資源的許可權,由於互斥物件只有一個,因此就決定了任何情況下此共用資源都不會同時被多個執行緒所存取

常見是使用synchronized和互斥鎖ReentrantLock,前者編譯後生成monitorenter 和monitorexit指令,如果當前執行緒重複進行monitorenter那麼計數加1,多少次monitorenter那麼就是需要多少次monitorexit。計數為0表示放棄鎖,如果獲取鎖前被另外一個執行緒執行了monitorenter 那麼當前執行緒將一致被阻塞等待知道鎖被釋放。後者可以看我關於AQS的筆記

2.非阻塞同步

互斥同步屬於一種悲觀的並行策略(無論共用資料有無競爭,先上鎖)都會導致使用者態到核心態的切換。基於衝突檢測的樂觀並行策略——CAS(也有其他方式,但是提得最多的就是CAS了)比較並交換,CAS需要三個引數,第一個是共用資料的地址,第二個是預期的舊值,第三個是準備設定的新值,只有當前地址對應的值等於舊值的時候才會設定地址對應的值為新值,如果發現舊值不等於地址的值那麼將操作失敗。CAS的原子性由作業系統指令(如cmpxchg)來保證。Java中Unsafe類和原子類提供了CAS方法,可以看我關於原子類的筆記。

3.可重入程式碼

指在程式碼執行的任何時候都可以中斷它,去執行另外一段程式碼(包括呼叫自己)控制權回來的時候程式不會出現任何錯誤,對結果也不會有任何影響。(可重入程式碼,一般都不依賴於全域性變數,不依賴堆上的物件,狀態都從引數傳入,不呼叫其他不可重入的程式碼)有點類似無狀態的servlet

4.ThreadLocal

把共用資料的可見限制在一個執行緒之內,那麼無需同步也可以實現安全。關於ThreadLocal可以看我的相關原始碼學習

四丶鎖優化

1.自旋鎖 與自適應自旋

1.1自旋鎖是什麼,為什麼需要自旋鎖

互斥同步由於執行緒的掛起和回覆執行緒的操作都需要從使用者態和核心態的切換,導致jvm並行效能的降低。但是共用資料的鎖定狀態有時候只會持續一小段時間,為了加鎖去掛起和恢復執行緒並不值得。自旋鎖應運而生,不讓執行緒立馬掛起而是讓執行緒執行自旋讓執行緒等待鎖的釋放。

1.2自旋鎖的缺點和自適應自旋

自旋是執行緒執行迴圈,雖然避免了執行緒的切換,但是自旋是需要佔用處理器時間的,所以如果鎖被佔用的時間很短那麼自旋鎖能夠有一個很好的效果,但是如果鎖被霸佔的時間比較長,那麼執行緒一致進行自旋消耗cpu資源,將導致效能的浪費。因此自旋等待的時間需要有一定的限度,如果自旋的次數超過了限定的次數但是還是沒有獲取到鎖,這時候需要掛起執行緒。在這個背景下,JDK6進行了自適應自旋,自適應自旋意味著自旋的時間不在固定了,而是由上一次在同一個鎖上的自旋時間以及鎖擁有者的狀態來決定的。如果在同一個鎖上面,自旋等待剛剛獲取到了鎖,並且持有鎖的執行緒正在執行中,那麼虛擬機器器會認為這次自旋也很有可能再次成功,進而允許自旋等待持續相對更長的時間。如果一個鎖,自旋很少成功獲得鎖,那麼在後續對這個鎖的爭奪的時候將可能直接忽略自旋過程,避免處理器資源的浪費。有了自適應自旋,隨著程式執行的時間以及效能監控的完善,虛擬機器器對鎖的預測將越來越精準。

2.鎖消除

指虛擬器即時編譯器執行過程中,對於一段需要同步的程式碼,檢測其根本不存在共用資料的競爭,那麼將對競爭的鎖進行消除

public synchronized String concat(String a,String b){
	return a+b;	
}
//string的連線操作總是產生一個新的字串
//上面程式碼也許會被優化成棧中new一個 StringBuffer 進行append
//StringBuffer中的同步鎖,鎖的是StringBuffer物件,
//但是StringBuffer在棧中new出來的
//逃逸分析就發現動態作用域在concat內部那麼就不需要執行緒同步

虛擬機器器發現這段程式碼中,在堆中的資料都不會逃逸出被其他執行緒存取到,那麼就會視為棧上的資料對待,認為它們執行緒私有,那麼加鎖也就沒有了必要。

3.鎖粗化

隨讓我們通常推薦鎖的粒度儘可能小,從而提高效能,但是如果對一個物件重複的進行加鎖解鎖,甚至是在for迴圈中加鎖解鎖,那麼即使沒有執行緒競爭,頻繁進行互斥同步也是非常浪費效能,也許虛擬機器器會幫我們擴大鎖的範圍,比如擴大到for迴圈的外部,這樣只需要進行一次加鎖解鎖了

4.鎖升級

4.1Java物件頭

以下是Java物件的記憶體佈局

Java物件頭儲存的資訊有:

  • mark word儲存物件的hashCode,鎖資訊,分代年齡等
  • class pointer,儲存當前物件型別資料的指標
  • array Lenth:如果是陣列那麼還有這部分來儲存陣列的長度

其中markword儲存了許多和物件執行狀態相關的資料,且這一部分涉及的非常的靈活,在不同的情況下可以表示不同的資訊,下面是不同鎖狀態下32位元虛擬機器器markword儲存資訊的結構

4.2 鎖升級的概念

JDK6為了解決獲取鎖釋放鎖的效能消耗,引入了偏向鎖和輕量級鎖。鎖的狀態從低到高依次是無鎖,偏向鎖,輕量級鎖,重量級鎖

4.3偏向鎖

hotspot的作者經過研究發現,大多數情況下,鎖不僅不存在多執行緒競爭,還常常是由同一個執行緒多次獲取,那麼能不能讓鎖記錄這個執行緒,下次這個執行緒來直接獲取鎖即可,從而降低獲得鎖的代價。當一個執行緒範圍同步塊並且獲取鎖時,會在物件頭和站在棧幀中記錄儲存鎖偏向的執行緒ID,以後進入和退出同步塊的時候都不需要進行CAS操作加鎖和解鎖。

4.3.1 偏向鎖的獲取

首先直接看物件頭中是否儲存了當前執行緒的id,如果儲存了那麼當前執行緒拿到鎖,這就是偏向的含義,如果失敗,那麼再看下當前鎖標誌位是否是01,01表示無鎖或者偏向鎖,繼續判斷是否是偏向鎖,如果是偏向鎖那麼需要檢視是否儲存了當前執行緒id,如果沒有儲存任何執行緒id那就CAS設定執行緒id。如果當前時無鎖也是CAS設定執行緒Id

4.3.2 偏向鎖的復原

偏向鎖使用一種等待競爭出現的時候才釋放鎖的機制,當其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖。偏向鎖的復原需要等待全域性安全點(這個時間點沒有正在執行的位元組碼)首先需要暫停持有偏向鎖的執行緒,然後檢查持有偏向鎖的執行緒是否存活,如果持有的執行緒不處於活動狀態那麼設定為無鎖狀態,如果任然存活,那麼擁有偏向鎖的棧才會被執行,遍歷偏向物件的鎖記錄,棧中的鎖記錄和物件頭的markword要麼偏向其他執行緒,要麼恢復到無鎖,要麼標記物件不適合偏向鎖。後續被標記不適合偏向鎖那麼將使用輕量級鎖

4.4輕量級鎖

4.4.1 輕量級鎖的獲取

執行緒在執行同步塊之前,JVM會先在當前執行緒的棧幀中建立用於儲存鎖記錄的空間(lock record)並且將物件頭中的mark word 賦值到鎖記錄lock record中,這種操作叫做Displaced mark word。然後執行緒產品使用CAS把物件頭的mark word替換為指向鎖記錄的指標,如果成功那麼表示當前執行緒獲取到鎖,如果失敗,表示存在其他執行緒競爭鎖,當前執行緒將通過自旋的方式獲取鎖。輕量級鎖解決了競爭執行緒不多,並且鎖很快就釋放,這時候掛起喚醒執行緒不划算,通過自旋減少使用者態到核心態的切換。

4.4.2輕量級鎖解鎖

解鎖,使用CAS操作把Displaced Mark Word替換回到物件頭,如果成功表示沒有不存在競爭,如果失敗表示當前鎖存在競爭,那麼鎖會膨脹為重量級鎖

4.5鎖升級全流程

鎖升級全過程 (1)