天天看點

聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖

聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖

寫在前邊

  • 走到哪都有各種瑣事,在MySQL中咱已經聊透了各種瑣事 ->MySQL鎖機制&&事務,今天來看看Java裡邊的鎖更新過程,以及各種鎖之間的比較,悲觀樂觀,粗化消除~

四種鎖的Markword

聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖
聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖

優先程度

  • 偏向鎖->輕量級鎖-(先自旋不行再膨脹)>重量級鎖(不會自旋直接阻塞)

輕量級鎖

隻是棧中一個鎖對象,不是monitor這種重量級

輕量級鎖的使用場景是:如果一個對象雖然有多個線程要對它進行加鎖,但是加鎖的時間是錯開的(也就是沒有人可以競争的,是以不會出現阻塞的情況),那麼可以使用輕量級鎖來進行優化。輕量級鎖對使用者是透明的,即文法仍然是synchronized,假設有兩個方法同步塊,利用同一個對象加鎖

static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步塊 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步塊 B
}
}
複制代碼           
  1. 每次指向到synchronized代碼塊時,都會建立鎖記錄(Lock Record)對象,每個線程都會包括一個鎖記錄的結構,鎖記錄内部可以儲存對象的Mark Word(用來改變對象的lock record編碼)和對象引用reference (表示指向哪個對象)
聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖
  1. 讓鎖記錄中的Object reference指向對象,并且嘗試用CAS(compare and sweep)替換Object對象的Mark Word(表示加鎖) , 将對象的Mark Word更新為指向Lock Record的指針,并将Mark Word 的值存入鎖記錄中 (等同于将Lock Record裡的owner指針指向對象的Mark Word。)
聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖
  1. 如果cas替換成功,那麼對象的對象頭儲存的就是鎖記錄的位址和狀态01,如下所示
聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖
  1. 如果cas失敗,有兩種情況 如果是其它線程已經持有了該Object的輕量級鎖,那麼表示有競争,将進入鎖膨脹階段 如果是自己的線程已經執行了synchronized進行加鎖,那麼那麼再添加一條 Lock Record 作為重入的計數
且此時新的一條Lock Record中,對象的MarkWord為null(相當于被前一個搶了)
聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖
  1. 當線程退出synchronized代碼塊的時候,如果擷取的是取值為 null 的鎖記錄,表示有重入,這時重置鎖記錄,表示重入計數減一
聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖
  1. 當線程退出synchronized代碼塊的時候,如果擷取的鎖記錄取值不為 null,那麼**使用cas将Mark Word的值恢複給對象 ** 成功則解鎖成功 失敗,則說明輕量級鎖進行了鎖膨脹或已經更新為重量級鎖,進入重量級鎖解鎖流程

總結

  • 加鎖和解鎖都是用CAS來交換Lock Record

鎖膨脹

如果在嘗試加輕量級鎖的過程中,cas操作無法成功,這是有一種情況就是其它線程已經為這個對象加上了輕量級鎖,這是就要進行鎖膨脹,将輕量級鎖變成重量級鎖。

  1. 當 Thread-1 進行輕量級加鎖時,Thread-0 已經對該對象加了輕量級鎖
聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖
  1. 這時 Thread-1 加輕量級鎖失敗,進入鎖膨脹流程 即為對象申請Monitor鎖,讓Object指向重量級鎖位址,然後自己進入Monitor 的EntryList 變成BLOCKED阻塞狀态
聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖
  1. 當Thread-0 推出synchronized同步塊時,使用cas将Mark Word的值恢複給對象頭,失敗,那麼會進入重量級鎖的解鎖過程,即按照Monitor的位址找到Monitor對象,将Owner設定為null,喚醒EntryList 中的Thread-1線程

總流程

聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖

自旋優化

為了讓目前線程“稍等一下”,我們需讓目前線程進行自旋,如果在自旋完成後前面鎖定同步資源的線程已經釋放了鎖,那麼目前線程就可以不必阻塞而是直接擷取同步資源,進而避免切換線程的開銷。這就是自旋鎖。
聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖

重量級鎖競争的時候,還可以使用自旋來進行優化,如果目前線程自旋成功(即在自旋的時候持鎖的線程釋放了鎖),那麼目前線程就可以不用進行上下文切換就獲得了鎖

  1. 自旋重試成功的情況
聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖
  1. 自旋重試失敗的情況,自旋了一定次數還是沒有等到持鎖的線程釋放鎖
聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖

自旋會占用 CPU 時間,單核 CPU 自旋就是浪費,多核 CPU 自旋才能發揮優勢。在 Java 6 之後自旋鎖是自适應的,比如對象剛剛的一次自旋操作成功過,那麼認為這次自旋成功的可能性會高,就多自旋幾次;反之,就少自旋甚至不自旋,總之,比較智能。Java 7 之後不能控制是否開啟自旋功能

自适應自旋鎖

自旋鎖在JDK1.4.2中引入,使用-XX:+UseSpinning來開啟。JDK 6中變為預設開啟,并且引入了自适應的自旋鎖(适應性自旋鎖)。

自适應意味着自旋的時間(次數)不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀态來決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運作中,那麼虛拟機就會認為這次自旋也是很有可能再次成功,進而它将允許自旋等待持續相對更長的時間。如果對于某個鎖,自旋很少成功獲得過,那在以後嘗試擷取這個鎖時将可能省略掉自旋過程,直接阻塞線程,避免浪費處理器資源。

在自旋鎖中 另有三種常見的鎖形式: TicketLock、CLHlock和MCSlock

偏向鎖

在輕量級的鎖中,我們可以發現,如果同一個線程對同一2對象進行重入鎖時,也需要執行CAS操作,這是有點耗時的,是以java6開始引入了偏向鎖,隻有第一次使用CAS時将對象的Mark Word頭設定為入鎖線程ID,之後這個入鎖線程再進行重入鎖時,發現線程ID是自己的,那麼就不用再進行CAS來加鎖和解鎖了

聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖

偏向狀态

聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖

一個對象的建立過程

  1. 如果開啟了偏向鎖(預設是開啟的),那麼對象剛建立之後,Mark Word 最後三位的值101,并且這是它的Thread,epoch,age都是0,在加鎖的時候進行設定這些的值.
  2. 偏向鎖預設是延遲的,不會在程式啟動的時候立刻生效,如果想避免延遲,可以添加虛拟機參數來禁用延遲:-XX:BiasedLockingStartupDelay=0來禁用延遲
  3. 注意:處于偏向鎖的對象解鎖後,線程 id 仍存儲于對象頭中

加上虛拟機參數-XX:BiasedLockingStartupDelay=0進行測試

public static void main(String[] args) throws InterruptedException {
Test1 t = new Test1();
    //加鎖前
test.parseObjectHeader(getObjectHeader(t));
    //加鎖後
synchronized (t){
test.parseObjectHeader(getObjectHeader(t));
}
    //釋放鎖後
test.parseObjectHeader(getObjectHeader(t));
} 

//輸出結果如下,三次輸出的狀态碼都為101
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
    
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
    
biasedLockFlag (1bit): 1
LockFlag (2bit): 01 
複制代碼           

禁用偏向鎖

聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖

測試禁用:如果沒有開啟偏向鎖,那麼對象建立後最後三位的值為001,這時候它的hashcode,age都為0,hashcode是第一次用到hashcode時才指派的。在上面測試代碼運作時在添加 VM 參數-XX:-UseBiasedLocking禁用偏向鎖(禁用偏向鎖則優先使用輕量級鎖),退出synchronized狀态變回001

  1. 測試代碼:虛拟機參數-XX:-UseBiasedLocking
  2. 輸出結果如下,最開始狀态為001,然後加輕量級鎖變成00,最後恢複成001
biasedLockFlag (1bit): 0
LockFlag (2bit): 01
LockFlag (2bit): 00
biasedLockFlag (1bit): 0
LockFlag (2bit): 01 
複制代碼           

撤銷偏向鎖-hashcode方法

測試 hashCode:當調用對象的hashcode方法的時候就會撤銷這個對象的偏向鎖,因為使用偏向鎖時沒有位置存**hashcode**的值了 而輕量級鎖存在lockRecord,重量級鎖存在monitor

  1. 測試代碼如下,使用虛拟機參數-XX:BiasedLockingStartupDelay=0 ,確定我們的程式最開始使用了偏向鎖!但是結果顯示程式還是使用了輕量級鎖。
public static void main(String[] args) throws InterruptedException {
Test1 t = new Test1();
    //撤銷偏向鎖
t.hashCode();
test.parseObjectHeader(getObjectHeader(t));
synchronized (t){
test.parseObjectHeader(getObjectHeader(t));
}
test.parseObjectHeader(getObjectHeader(t));
} 

輸出結果
biasedLockFlag (1bit): 0
LockFlag (2bit): 01
    
LockFlag (2bit): 00
biasedLockFlag (1bit): 0
    
LockFlag (2bit): 01 
複制代碼           

撤銷偏向鎖-其它線程使用對象

這裡我們示範的是偏向鎖撤銷變成輕量級鎖的過程,那麼就得滿足輕量級鎖的使用條件,就是沒有線程對同一個對象進行鎖競争,我們使用wait 和 notify 來輔助實作

  1. 代碼,虛拟機參數-XX:BiasedLockingStartupDelay=0確定我們的程式最開始使用了偏向鎖!
  2. 輸出結果,最開始使用的是偏向鎖,但是第二個線程嘗試擷取對象鎖時,發現本來對象偏向的是線程一,那麼偏向鎖就會失效,加的就是輕量級鎖
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
LockFlag (2bit): 00
biasedLockFlag (1bit): 0
LockFlag (2bit): 01 
複制代碼           

撤銷偏向鎖 - 調用 wait/notify

會使對象的鎖變成重量級鎖,因為wait/notify方法隻有重量級鎖才支援

批量重偏向

如果對象被多個線程通路,但是沒有競争,這時候偏向了線程一的對象又有機會重新偏向線程二,即可以不用更新為輕量級鎖,可這和我們之前做的實驗沖突了呀,其實要實作重新偏向是要有條件的:就是超過20對象對同一個線程如線程一撤銷偏向時,那麼第20個及以後的對象才可以将撤銷對線程一的偏向這個動作變為将第20個及以後的對象偏向線程二。

樂觀鎖VS悲觀鎖

樂觀鎖(無鎖)

CAS

優點

  • 不會出現阻塞,所有線程都處于競争狀态,适用于線程較小的情況

缺點

  • 當線程較多的時候,會不斷自旋浪費cpu資源

多讀用樂觀鎖(沖突少)

多寫用悲觀鎖(沖突多)

從上面對兩種鎖的介紹,我們知道兩種鎖各有優缺點,不可認為一種好于另一種,像樂觀鎖适用于寫比較少的情況下(多讀場景),即沖突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果是多寫的情況,一般會經常産生沖突,這就會導緻上層應用會不斷的進行 retry,這樣反倒是降低了性能,是以一般多寫的場景下用悲觀鎖就比較合适。

公平鎖VS非公平鎖

公平鎖

公平鎖的優點是等待鎖的線程不會餓死。缺點是整體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。

非公平鎖

非公平鎖的優點是可以減少喚起線程的開銷(比如新的線程D進來的時候剛好前邊的線程A釋放了鎖,那麼D可以直接擷取鎖,無需進入阻塞隊列),整體的吞吐效率高,因為線程有幾率不阻塞直接獲得鎖,CPU不必喚醒所有線程。缺點是處于等待隊列中的線程可能會餓死,或者等很久才會獲得鎖。

實作

ReentrantLock提供了公平和非公平鎖的實作。· 公平鎖:ReentrantLockpairLock =new ReentrantLock(true)。· 非公平鎖:ReentrantLockpairLock =new ReentrantLock(false)。

  • 如果構造函數不傳遞參數,則預設是非公平鎖。

源碼比較

聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖

通過上圖中的源代碼對比,我們可以明顯的看出公平鎖與非公平鎖的lock()方法唯一的差別就在于公平鎖在擷取同步狀态時多了一個限制條件:hasQueuedPredecessors()。

聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖

再進入hasQueuedPredecessors(),可以看到該方法主要做一件事情:主要是判斷目前線程是否位于同步隊列中的第一個。如果是則傳回true,否則傳回false。

綜上,公平鎖就是通過同步隊列來實作多個線程按照申請鎖的順序來擷取鎖,進而實作公平的特性。非公平鎖加鎖時不考慮排隊等待問題,直接嘗試擷取鎖,是以存在後申請卻先獲得鎖的情況。

可重入鎖vs不可重入鎖

不可重入鎖可能會導緻死鎖問題

首先ReentrantLock和NonReentrantLock都繼承父類AQS,其父類AQS中維護了一個同步狀态status來計數重入次數,status初始值為0。

當線程嘗試擷取鎖時,可重入鎖先嘗試擷取并更新status值,如果status == 0表示沒有其他線程在執行同步代碼,則把status置為1,目前線程開始執行。如果status != 0,則判斷目前線程是否是擷取到這個鎖的線程,如果是的話執行status+1,且目前線程可以再次擷取鎖。而非可重入鎖是直接去擷取并嘗試更新目前status的值,如果status != 0的話會導緻其擷取鎖失敗,目前線程阻塞。

釋放鎖時,可重入鎖同樣先擷取目前status的值,在目前線程是持有鎖的線程的前提下。如果status-1 == 0,則表示目前線程所有重複擷取鎖的操作都已經執行完畢,然後該線程才會真正釋放鎖。而非可重入鎖則是在确定目前線程是持有鎖的線程之後,直接将status置為0,将鎖釋放。

聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖

鎖消除和鎖粗化

blog.csdn.net/qq_26222859…

鎖消除

鎖消除是發生在編譯器級别的一種鎖優化方式。

有時候我們寫的代碼完全不需要加鎖,卻執行了加鎖操作。

鎖消除是Java虛拟機在JIT編譯時,通過對運作上下文的掃描,去除不可能存在共享資源競争的鎖,通過鎖消除,可以節省毫無意義的請求鎖時間。

比如,StringBuffer類的append操作:

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}
複制代碼           

從源碼中可以看出,append方法用了synchronized關鍵詞,它是線程安全的。但我們可能僅線上程内部把StringBuffer當作局部變量使用,比如:

public static String test(String str1, String str2) {
        StringBuffer sb = new StringBuffer();
        sb.append(str1);
        sb.append(str2);
        return sb.toString();
    }
}
複制代碼           

此時不同線程調用該方法,都會建立不同的stringbuffer對象,并不會出現鎖競争等同步問題,是以此時編譯器會做優化,去除不可能存在共享資源競争的鎖,這便是鎖消除。

鎖削除的主要判定依據來源于逃逸分析的資料支援,如果判斷到一段代碼中,在堆上的所有資料都不會逃逸出去被其他線程通路到,那就可以把它們當作棧上資料對待,認為它們是線程私有的,同步加鎖自然就無須進行。

鎖粗化

public void doSomethingMethod(){
    synchronized(lock){
        //do some thing
    }
	//兩個加鎖過程中間,還有一些代碼,但執行的速度很快
    
    synchronized(lock){
        //do other thing
    }
}
複制代碼           

這兩塊需要同步操作的代碼之間,需要做一些其它的工作,而這些工作隻會花費很少的時間,那麼我們就可以把這些工作代碼放入鎖内,将兩個同步代碼塊合并成一個,以降低多次鎖請求、同步、釋放帶來的系統性能消耗,合并後的代碼如下:

public void doSomethingMethod(){
    //進行鎖粗化:整合成一次鎖請求、同步、釋放
    synchronized(lock){
        //do some thing
        //做其它不需要同步但能很快執行完的工作
        //do other thing
    }
}
複制代碼           

手撕面答環節 -- 這是一條分割線

synchronized怎麼保證可見性?

  • 線程加鎖前,将清空工作記憶體中共享變量的值,進而使用共享變量時需要從主記憶體中重新讀取最新的值。
  • 線程加鎖後,其它線程無法擷取主記憶體中的共享變量。
  • 線程解鎖前,必須把共享變量的最新值重新整理到主記憶體中。

synchronized怎麼保證有序性?

synchronized同步的代碼塊,具有排他性,一次隻能被一個線程擁有,是以synchronized保證同一時刻,代碼是單線程執行的。

因為as-if-serial語義的存在,單線程的程式能保證最終結果是有序的,但是不保證不會指令重排。

是以synchronized保證的有序是執行結果的有序性,而不是防止指令重排的有序性。

synchronized怎麼實作可重入的呢?

synchronized 是可重入鎖,也就是說,允許一個線程二次請求自己持有對象鎖的臨界資源,這種情況稱為可重入鎖。

synchronized 鎖對象的時候有個計數器,他會記錄下線程擷取鎖的次數,在執行完對應的代碼塊之後,計數器就會-1,直到計數器清零,就釋放鎖了。

之是以,是可重入的。是因為 synchronized 鎖對象有個計數器,會随着線程擷取鎖後 +1 計數,當線程執行完畢後 -1,直到清零釋放鎖。

鎖更新?synchronized優化了解嗎?

Java對象頭裡,有一塊結構,叫Mark Word标記字段,這塊結構會随着鎖的狀态變化而變化。

64 位虛拟機 Mark Word 是 64bit,我們來看看它的狀态變化:

聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖

Mark Word存儲對象自身的運作資料,如哈希碼、GC分代年齡、鎖狀态标志、偏向時間戳(Epoch) 等。

  • 偏向鎖:在無競争的情況下,隻是在Mark Word裡存儲目前線程指針,CAS操作都不做。
  • 輕量級鎖:在沒有多線程競争時,相對重量級鎖,減少作業系統互斥量帶來的性能消耗。但是,如果存在鎖競争,除了互斥量本身開銷,還額外有CAS操作的開銷。
  • 自旋鎖:減少不必要的CPU上下文切換。在輕量級鎖更新為重量級鎖時,就使用了自旋加鎖的方式
  • 鎖粗化:将多個連續的加鎖、解鎖操作連接配接在一起,擴充成一個範圍更大的鎖。
  • 鎖消除:虛拟機即時編譯器在運作時,對一些代碼上要求同步,但是被檢測到不可能存在共享資料競争的鎖進行消除。

更新的具體過程

首先是無鎖,沒有競争的情況

偏向鎖

再是偏向鎖,判斷是否可以偏向,檢視線程ID是否為目前線程,是的話則直接執行,無需CAS

不是則CAS争奪鎖,若成功則設定線程ID為自己 失敗,則更新為輕量級鎖

偏向鎖的撤銷

  1. 偏向鎖不會主動釋放(撤銷),隻有遇到其他線程競争時才會執行撤銷,由于撤銷需要知道目前持有該偏向鎖的線程棧狀态,是以要等到safepoint時執行,此時持有該偏向鎖的線程(T)有‘2’,‘3’兩種情況;
  2. 撤銷----T線程已經退出同步代碼塊,或者已經不再存活,則直接撤銷偏向鎖,變成無鎖狀态----該狀态達到門檻值20則執行批量重偏向
  3. 更新----T線程還在同步代碼塊中,則将T線程的偏向鎖更新為輕量級鎖,目前線程執行輕量級鎖狀态下的鎖擷取步驟----該狀态達到門檻值40則執行批量撤銷

輕量級鎖

  1. 進行加鎖操作時,jvm會判斷是否已經時重量級鎖,如果不是,則會在目前線程棧幀中劃出一塊空間,作為該鎖的鎖記錄,并且将鎖對象MarkWord複制到該鎖記錄中
  2. 複制成功之後,jvm使用CAS操作将對象頭MarkWord更新為指向鎖記錄的指針,并将鎖記錄裡的owner指針指向對象頭的MarkWord。如果成功,則執行‘3’,否則執行‘4’
  3. 更新成功,則目前線程持有該對象鎖,并且對象MarkWord鎖标志設定為‘00’,即表示此對象處于輕量級鎖狀态
  4. 更新失敗,jvm先檢查對象MarkWord是否指向目前線程棧幀中的鎖記錄,如果是則執行‘5’,否則執行‘6’
  5. 表示鎖重入;然後目前線程棧幀中增加一個鎖記錄第一部分(Displaced Mark Word)為null,并指向Mark Word的鎖對象,起到一個重入計數器的作用。
  6. 表示該鎖對象已經被其他線程搶占,則進行自旋等待(預設10次),等待次數達到門檻值仍未擷取到鎖,則更新為重量級鎖

更新過程:

聊聊Java中的鎖更新過程——無鎖>偏向鎖>輕量級鎖>重量級鎖
本篇屬于是冷面大翻炒了,如有錯誤的地方還請指正

作者:Melo_

連結:https://juejin.cn/post/7160296578173894663

繼續閱讀