【java併發程式設計實戰4】偏向鎖-輕量鎖-重量鎖的那點祕密(synchronize實現原理)

weixin_34008784發表於2018-09-06

目錄

【SpringBoot2.0文章彙總目錄,java多執行緒教程文章彙總 長期更新系列

請多多支援

在多執行緒併發程式設計中,synchronized一直都是元老級別的角色,人們都通常稱呼它為重量鎖,但是在jdk1.6版本之後,jdk就對synchronized做了大量的優化,這時候我們就不能稱呼它為重量鎖了,有的時候它也是很的,那麼接下來我們就調調,synchronized是怎麼被優化的,它跟偏向鎖、輕量鎖、重量鎖又有什麼淵源。

synchronized

回顧一下synchronized是怎麼使用的呢。

1、同步普通方法

public synchronized void  sync1() {
    // do somethings
}

在該方法中,synchronized鎖的是當前例項的物件

2、同步靜態方法

public static synchronized void sync2() {
    // do somethings
}

由於該方法是一個靜態方法,那麼它鎖的當前類的class物件。

3、同步方法塊

public void sync3() {
    synchronized(this) {
        // do somethings
    }
}

public void sync4() {
    synchronized(MyTest.css) {
        // do somethings
    }
}

那麼同步方法塊是需要根據方法中具體同步的物件來實現的。

在上面程式碼中其實sync3()跟同步普通方法一樣,鎖的是當前例項物件;那麼sync4方法就與同步靜態方法一樣,鎖的是當前類的class物件。

從上面程式碼可以看出來的,我們通過使用synchronized關鍵字可以很簡單的解決併發問題,但是其實是jvm底層通過使用一種叫內建鎖的手段,簡化了開發人員實現併發的複雜度,在jdk1.6以前 synchronized是基於重量鎖實現的,即每次遇到同步程式碼都要獲取鎖,然後釋放鎖,在jdk1.6之後對其優化,根據不同場景使用不同的策略,這也就是 偏向鎖、輕量鎖、重量鎖的來由。在介紹他們之前我先介紹一下另一個鎖-自旋鎖。聽到這麼多鎖,是不是頭暈,當初我學習的時候也是這樣的。但是當你慢慢學習深入,你就會很容易的理解每個鎖的作用。

自旋鎖

自旋鎖顧名思義,就是自己旋轉轉圈等待,那麼它有什麼作用呢?

  • 當前執行緒嘗試去競爭鎖
  • 競爭失敗,準備阻塞自己
  • 但是並沒有阻塞自己,而是採用自旋鎖,進入自旋狀態
  • 進入自旋狀態,並且重新不斷競爭鎖
  • 如果在自旋期間成功獲取鎖,那麼結束自旋狀態,否則進入阻塞狀態

如果在自旋期間成功獲取鎖,那麼就減少一次執行緒的切換。

根據上面解釋我們可以很容易的明白自旋鎖的意義,因為cpu從核心態切換至使用者態,執行緒的阻塞與恢復會浪費資源的,但是通過自旋而不是去阻塞當前執行緒,那麼就會節省這個一個cpu狀態切換。

所以自旋鎖適合在** 持有鎖的時間長,且競爭不激烈**的場景下使用。

使用-XX:-UseSpinning引數關閉自旋鎖優化;-XX:PreBlockSpin引數修改預設的自旋次數

偏向鎖

在實際場景中,如果一個同步方法,沒有多執行緒競爭,並且總是由同一個執行緒多次獲取鎖,如果每次還有阻塞執行緒,喚醒cpu從使用者態轉核心態,那麼對於cpu是一種資源的浪費,為了解決這類問題,舊引入了偏向鎖的概念。

“偏向”的意思是,偏向鎖假定將來只有第一個申請鎖的執行緒會使用鎖(不會有任何執行緒再來申請鎖),因此,只需要在Mark Word中CAS記錄owner(本質上也是更新,但初始值為空),如果記錄成功,則偏向鎖獲取成功,記錄鎖狀態為偏向鎖,以後當前執行緒等於owner就可以零成本的直接獲得鎖;否則,說明有其他執行緒競爭,膨脹為輕量級鎖

具體的步驟如下

  • 訪問同步程式碼塊

  • 檢查物件頭是否owner是否儲存當前現成的id

  • 如果沒有,進行CAS嘗試替換mark word中的owner 如果有執行同步程式碼(代表獲取鎖成功)

  • 修改成功 (代表無競爭)owner修改為當前執行緒id,執行同步程式碼 修改失敗(代表有競爭) 進入撤銷偏向鎖,暫停執行緒並將owner置空,進入輕量鎖。

5338436-c63dbfb4898348a7.png
899685-20161025102843468-151954717.png

偏向鎖無法使用自旋鎖優化,因為一旦有其他執行緒申請鎖,就破壞了偏向鎖的假定。

如果你確定應用程式中所有的鎖通常是在競爭狀態,你可以通過JVM引數關閉偏向鎖

UseBiasedLocking = false,那麼程式會預設進入輕量鎖狀態。

輕量鎖

如果說偏向鎖是為了解決同步程式碼在單執行緒下訪問效能問題,那麼輕量鎖是為了解決減少無實際競爭情況下,使用重量級鎖產生的效能消耗

輕量鎖,顧名思義,輕量是相對於重量的問題,使用輕量鎖時,不需要申請互斥量(mutex)

,而是將mark word中的資訊複製到當前執行緒的棧中,然後通過cas嘗試修改mark word並替換成輕量鎖,如果替換成功則執行同步程式碼。如果此時有執行緒2來競爭,並且他也嘗試cas修改mark word但是失敗了,那麼執行緒2會進入自旋狀態,如果在自旋狀態也沒有修改成功,那麼輕量鎖將膨脹成狀態,mark word會被修改成重量鎖標記(10) ,執行緒進入阻塞狀態。

當然,由於輕量級鎖天然瞄準不存在鎖競爭的場景,如果存在鎖競爭但不激烈,仍然可以用自旋鎖優化,自旋失敗後再膨脹為重量級鎖

重量鎖

在jvm規範中,synchronized是基於監視器鎖(monitor)來實現的,它會在同步程式碼之前新增一個monitorenter指令,獲取到該物件的monitor,同時它會在同步程式碼結束處和異常處新增一個monitorexit指令去釋放該物件的monitor,需要注意的是每一個物件都有一個monitor與之配對,當一個monitor被獲取之後 也就是被monitorenter,它會處於一個鎖定狀態,其他嘗試獲取該物件的monitor的執行緒會獲取失敗,只有當獲取該物件的monitor的執行緒執行了monitorexit指令後,其他執行緒才有可能獲取該物件的monitor成功。

所以從上面描述可以得出,監視器鎖就是monitor它是互斥的(mutex)。由於它是互斥的,那麼它的操作成本就非常的高,包括系統呼叫引起的核心態與使用者態切換、執行緒阻塞造成的執行緒切換等。因此,後來稱這種鎖為“重量級鎖”。

小結

偏向鎖、輕量級鎖、重量級鎖適用於不同的併發場景:

  • 偏向鎖:無實際競爭,且將來只有第一個申請鎖的執行緒會使用鎖。
  • 輕量級鎖:無實際競爭,多個執行緒交替使用鎖;允許短時間的鎖競爭。
  • 重量級鎖:有實際競爭,且鎖競爭時間長。

另外,如果鎖競爭時間短,可以使用自旋鎖進一步優化輕量級鎖、重量級鎖的效能,減少執行緒切換。

如果鎖競爭程度逐漸提高(緩慢),那麼從偏向鎖逐步膨脹到重量鎖,能夠提高系統的整體效能。
同時需要注意鎖可以升級,但是不能降級

另外通過這次學習,大家應該也知道自從jdk1.6以後 synchronized已經被優化了,效能不會比Lock

所以jdk.16版本及其以後版本的同學可以放心大膽的使用了。

最後附一張從偏向鎖膨脹至重量鎖的完全的流程圖

5338436-59509e312fc6e64a.png
4491294-e3bcefb2bacea224.png

最後歡迎大家關注一下我的個人公眾號。一起交流一起學習,有問必答。


5338436-ddb4dd1530787751.png
公眾號

相關文章