爲了換取性能,JVM在內置鎖上作了很是多的優化,膨脹式的鎖分配策略就是其一。理解偏向鎖、輕量級鎖、重量級鎖的要解決的基本問題,幾種鎖的分配和膨脹過程,有助於編寫並優化基於鎖的併發程序。java
內置鎖的分配和膨脹過程較爲複雜,限於時間和精力,文中該部份內容是根據網上的多方資料整合而來;僅爲方便查閱,後面繼續分析JVM源碼的時候也有個參考。若是對各級鎖已經有了基本瞭解,讀者大可跳過此文。git
內置鎖是JVM提供的最便捷的線程同步工具,在代碼塊或方法聲明上添加synchronized關鍵字便可使用內置鎖。使用內置鎖可以簡化併發模型;隨着JVM的升級,幾乎不須要修改代碼,就能夠直接享受JVM在內置鎖上的優化成果。從簡單的重量級鎖,到逐漸膨脹的鎖分配策略,使用了多種優化手段解決隱藏在內置鎖下的基本問題。github
內置鎖在Java中被抽象爲監視器鎖(monitor)。在JDK 1.6以前,監視器鎖能夠認爲直接對應底層操做系統中的互斥量(mutex)。這種同步方式的成本很是高,包括系統調用引發的內核態與用戶態切換、線程阻塞形成的線程切換等。所以,後來稱這種鎖爲「重量級鎖」。性能優化
首先,內核態與用戶態的切換上不容易優化。但經過自旋鎖,能夠減小線程阻塞形成的線程切換(包括掛起線程和恢復線程)。併發
若是鎖的粒度小,那麼鎖的持有時間比較短(儘管具體的持有時間沒法得知,但能夠認爲,一般有一部分鎖能知足上述性質)。那麼,對於競爭這些鎖的而言,由於鎖阻塞形成線程切換的時間與鎖持有的時間至關,減小線程阻塞形成的線程切換,能獲得較大的性能提高。具體以下:函數
若是在自旋的時間內,鎖就被舊owner釋放了,那麼當前線程就不須要阻塞本身(也不須要在將來鎖釋放時恢復),減小了一次線程切換。工具
「鎖的持有時間比較短」這一條件能夠放寬。實際上,只要鎖競爭的時間比較短(好比線程1快釋放鎖的時候,線程2纔會來競爭鎖),就可以提升自旋得到鎖的機率。這一般發生在鎖持有時間長,但競爭不激烈的場景中。性能
使用-XX:-UseSpinning參數關閉自旋鎖優化;-XX:PreBlockSpin參數修改默認的自旋次數。優化
自適應意味着自旋的時間再也不固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定:操作系統
自適應自旋解決的是「鎖競爭時間不肯定」的問題。JVM很難感知到確切的鎖競爭時間,而交給用戶分析就違反了JVM的設計初衷。自適應自旋假定不一樣線程持有同一個鎖對象的時間基本至關,競爭程度趨於穩定,所以,能夠根據上一次自旋的時間與結果調整下一次自旋的時間。
然而,自適應自旋也沒能完全解決該問題,若是默認的自旋次數設置不合理(太高或太低),那麼自適應的過程將很難收斂到合適的值。
自旋鎖的目標是下降線程切換的成本。若是鎖競爭激烈,咱們不得不依賴於重量級鎖,讓競爭失敗的線程阻塞;若是徹底沒有實際的鎖競爭,那麼申請重量級鎖都是浪費的。輕量級鎖的目標是,減小無實際競爭狀況下,使用重量級鎖產生的性能消耗,包括系統調用引發的內核態與用戶態切換、線程阻塞形成的線程切換等。
顧名思義,輕量級鎖是相對於重量級鎖而言的。使用輕量級鎖時,不須要申請互斥量,僅僅_將Mark Word中的部分字節CAS更新指向線程棧中的Lock Record,若是更新成功,則輕量級鎖獲取成功_,記錄鎖狀態爲輕量級鎖;不然,說明已經有線程得到了輕量級鎖,目前發生了鎖競爭(不適合繼續使用輕量級鎖),接下來膨脹爲重量級鎖。
Mark Word是對象頭的一部分;每一個線程都擁有本身的線程棧(虛擬機棧),記錄線程和函數調用的基本信息。兩者屬於JVM的基礎內容,此處不作介紹。
固然,因爲輕量級鎖自然瞄準不存在鎖競爭的場景,若是存在鎖競爭但不激烈,仍然能夠用自旋鎖優化,自旋失敗後再膨脹爲重量級鎖。
同自旋鎖類似:
在沒有實際競爭的狀況下,還可以針對部分場景繼續優化。若是不只僅沒有實際競爭,自始至終,使用鎖的線程都只有一個,那麼,維護輕量級鎖都是浪費的。偏向鎖的目標是,減小無競爭且只有一個線程使用鎖的狀況下,使用輕量級鎖產生的性能消耗。輕量級鎖每次申請、釋放鎖都至少須要一次CAS,但偏向鎖只有初始化時須要一次CAS。
「偏向」的意思是,偏向鎖假定未來只有第一個申請鎖的線程會使用鎖(不會有任何線程再來申請鎖),所以,只須要在Mark Word中CAS記錄owner(本質上也是更新,但初始值爲空),若是記錄成功,則偏向鎖獲取成功,記錄鎖狀態爲偏向鎖,之後當前線程等於owner就能夠零成本的直接得到鎖;不然,說明有其餘線程競爭,膨脹爲輕量級鎖。
偏向鎖沒法使用自旋鎖優化,由於一旦有其餘線程申請鎖,就破壞了偏向鎖的假定。
一樣的,若是明顯存在其餘線程申請鎖,那麼偏向鎖將很快膨脹爲輕量級鎖。
不過這個反作用已經小的多。
若是須要,使用參數-XX:-UseBiasedLocking禁止偏向鎖優化(默認打開)。
偏向鎖、輕量級鎖、重量級鎖分配和膨脹的詳細過程見後。會涉及一些Mark Word與CAS的知識。
偏向鎖、輕量級鎖、重量級鎖適用於不一樣的併發場景:
另外,若是鎖競爭時間短,可使用自旋鎖進一步優化輕量級鎖、重量級鎖的性能,減小線程切換。
若是鎖競爭程度逐漸提升(緩慢),那麼從偏向鎖逐步膨脹到重量鎖,可以提升系統的總體性能。
重申,這部分主要是根據網上的多方資料整理。核心是這位巨巨整理的流程圖,至關詳細,基本符合邏輯。
前面講述了內置鎖在使用過程當中的一些基本問題和解決方案,實現原理一筆帶過。詳細的鎖分配和膨脹過程以下:
圖中有一處疑問:
按照圖中流程,若是發現鎖已經膨脹爲重量級鎖,就直接使用互斥量mutex阻塞當前線程。
然而,自旋鎖的一大好處就是減小線程切換的開銷。在這裏沒有必要直接阻塞當前線程,大能夠像輕量級鎖同樣,自旋一會,失敗了再阻塞。
特別說明兩點:
expected == null
,newValue == ownerThreadId
,所以,只有第一個申請偏向鎖的線程可以返回成功,後續線程都必然失敗(部分線程檢測到可偏向,同時嘗試CAS記錄owner)。另外,當重量級鎖被解除後,須要喚醒一個被阻塞的線程,這部分邏輯與ReentrantLock基本相同,詳見源碼|併發一枝花之ReentrantLock與AQS(1):lock、unlock。
上圖記載的很詳細,也有Mark Word的圖解。看懂上圖後,再來看《深刻理解Java虛擬機:JVM高級特性與最佳實踐(第2版)》中的簡化版流程圖就能看懂了:
挖坑:
簡化版中指出了
重偏向
過程。這一過程對於性能優化和膨脹過程都很是重要;但若是考慮重偏向的話,可能上述特別說明的內容就不成立了。要整理的筆記太多啦時間不夠啊,猴子選擇暫時放棄這個問題,,,恩,挖個坑,之後再追源碼填坑。重偏向和epoch的做用參考:
參考:
本文連接:淺談偏向鎖、輕量級鎖、重量級鎖
做者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議發佈,歡迎轉載,演繹或用於商業目的,可是必須保留本文的署名及連接。