【嗅探底層】你知道Synchronized做用是同步加鎖,可你知道它在JVM中是如何實現的嗎?


​本文系公衆號石杉的架構筆記的讀者投稿算法

做者:李瑞傑安全

目前任職於阿里巴巴,資深JVM研究人員架構


友情提示:性能

本文內容涉及JVM底層,文章燒腦,請謹慎閱讀!學習


咱們能夠利用synchronized關鍵字來對程序進行加鎖。它既能夠用來聲明一個synchronized代碼塊,也能夠直接標記靜態方法或者實例方法。優化

當談到synchronized時,咱們有必要了解字節碼中的monitorenter和monitorexit指令。this

這兩種指令均會消耗操做數棧上的一個引用類型的元素(也就是 synchronized關鍵字括號裏的引用),做爲所要加鎖解鎖的鎖對象。操作系統

下面咱們將深刻了解Synchronized在JVM底層的實現原理。線程

考察如下的代碼:設計

查看這個代碼編譯後的字節碼,我就直接用下面這張圖解釋了。

ps:截圖截得不太好,下面有點沒截到,你們湊合看看:


你可能會留意到,上面的字節碼中包含一個 monitorenter 指令以及多個 monitorexit 指令。

這是由於 Java 虛擬機須要確保所得到的鎖在正常執行路徑以及異常執行路徑上都可以被解鎖。

你們能夠看個人註釋,本身思考一下,應該都能看懂。

應該注意,若是用synchronized標記方法,你會看到字節碼中方法的訪問標記包括ACC_SYNCHRONIZED。

該標記表示在進入該方法時,Java 虛擬機須要進行monitorenter操做。

而在退出該方法時,不論是正常返回,仍是向調用者拋異常,Java 虛擬機均須要進行monitorexit操做。

用兩張圖一看就懂。



能夠看到,在0號字節碼處就返回了。

這裏有人可能問了,這裏沒有調用monitorenter和monitorexit指令啊?怎麼實現的加鎖?

要注意,這裏monitorenter 和 monitorexit 操做所對應的鎖對象是隱式的。

對於實例方法來講,這兩個操做對應的鎖對象是 this;對於靜態方法來講,這兩個操做對應的鎖對象則是所在類的Class實例。


咱們先來介紹Synchronized的重入的實現機理。


能夠認爲每一個鎖對象擁有一個鎖計數器和一個指向持有該鎖的線程的指針。

當執行monitorenter時,若是目標鎖對象的計數器爲0,那麼說明它沒有被其餘線程所持有。

Java虛擬機會將該鎖對象的持有線程設置爲當前線程,而且將其計數器加1。

在目標鎖對象的計數器不爲 0 的狀況下,若是鎖對象的持有線程是當前線程,那麼 Java 虛擬機能夠將其計數器加1,不然須要等待,直至持有線程釋放該鎖。

當執行monitorexit時,Java虛擬機則需將鎖對象的計數器減1。計數器爲0,表明鎖已被釋放。


這就是鎖的重入的實現機理。


說完了這個實現機理,咱們來探究具體的鎖實現。

首先談談重量級鎖,重量級鎖是 Java 虛擬機中最爲基礎的鎖實現。

在這種狀態下,Java 虛擬機會阻塞加鎖失敗的線程,而且在目標鎖被釋放的時候,喚醒這些線程。在Linux中,這是經過pthread庫的互斥鎖來實現的。

此外,這些操做將涉及系統調用,須要從操做系統的用戶態切換至內核態,其開銷很是之大。

爲了儘可能避免昂貴的線程阻塞、喚醒操做,Java虛擬機會在線程進入阻塞狀態以前,以及被喚醒後競爭不到鎖的狀況下,進入自旋狀態,在處理器上空跑而且輪詢鎖是否被釋放。

若是此時鎖剛好被釋放了,那麼當前線程便無須進入阻塞狀態,而是直接得到這把鎖。

下面我將介紹自適應自旋的概念,剛纔說了自旋是什麼,可是自旋很耗費資源,因此咱們能夠根據以往自旋等待時是否可以得到鎖,來動態調整自旋的時間(循環數目)。

因此Synchronized是否公平這個問題能夠休矣,爲何呢?

處於阻塞狀態的線程,並無辦法馬上競爭被釋放的鎖。然而,處於自旋狀態的線程,則頗有可能優先得到這把鎖。因此Synchronized不是公平的

咱們再介紹輕量級鎖,針對多個線程在不一樣的時間段請求同一把鎖,也就是說沒有鎖競爭。

針對這種情形,Java 虛擬機採用了輕量級鎖,來避免重量級鎖的阻塞以及喚醒。

在介紹輕量級鎖的原理以前,咱們先來了解一下Java虛擬機是怎麼區分輕量級鎖和重量級鎖的。

簡單的說,對象頭中有一個標記字段。它的最後兩位便被用來表示該對象的鎖狀態,其中:

  • 00表明輕量級鎖

  • 01表明無鎖(或偏向鎖)

  • 10表明重量級鎖

  • 11則跟垃圾回收算法的標記有關。


當進行加鎖操做時,Java虛擬機會判斷是否已是重量級鎖。

若是不是,它會在當前線程的當前棧楨中劃出一塊空間,做爲該鎖的鎖記錄,而且將鎖對象的標記字段複製到該鎖記錄中。

而後,Java 虛擬機會嘗試用 CAS 操做替換鎖對象的標記字段。

各位有興趣能夠了解一下JVM的CAS在X86機器上的實現,是彙編指令lock cmpxhcg

這裏我簡單介紹一下,CAS 是一個原子操做,它會比較目標地址的值是否和指望值相等,若是相等,則替換爲一個新的值。

假設當前鎖對象的標記字段爲 X…XYZ,Java 虛擬機會比較該字段是否爲 X…X01。

若是是,則替換爲剛纔分配的鎖記錄的地址。因爲內存對齊的緣故,它的最後兩位爲 00。此時,該線程已成功得到這把鎖,能夠繼續執行了。

若是不是 X…X01,那麼有兩種可能:

  • 第一,該線程重複獲取同一把鎖。此時,Java 虛擬機會將0加入鎖記錄,以表明該鎖被重複獲取。

  • 第二,其餘線程持有該鎖。此時,Java 虛擬機會將這把鎖膨脹爲重量級鎖,而且阻塞當前線程。

你能夠將一個線程的全部鎖記錄想象成一個棧結構,每次加鎖壓入一條鎖記錄,解鎖彈出一條鎖記錄,當前鎖記錄指的即是棧頂的鎖記錄。

當進行解鎖操做時,若是當前鎖記錄的值爲 0,則表明重複進入同一把鎖,直接返回便可。

若當前鎖記錄不是0,Java 虛擬機會嘗試用 CAS 操做,比較鎖對象的標記字段的值是否爲當前鎖記錄的地址。

若是是,則替換爲鎖記錄中的值,也就是鎖對象本來的標記字段。此時,該線程已經成功釋放這把鎖。

若是不是,則意味着這把鎖已經被膨脹爲重量級鎖。此時,Java 虛擬機會進入重量級鎖的釋放過程,喚醒因競爭該鎖而被阻塞了的線程。

下面咱們介紹偏向鎖,偏向鎖針對的是從始至終只有一個線程請求某一把鎖。是輕量級鎖的更進一步的樂觀狀況。

在線程進行加鎖時,若是該鎖對象支持偏向鎖,那麼 Java 虛擬機會經過 CAS 操做,將當前線程的地址記錄在鎖對象的標記字段之中,而且將標記字段的最後三位設置爲 101。

這裏介紹一下epoch的概念,每一個類中維護一個epoch值,你能夠理解爲這個類全部實例對象的第幾代偏向鎖。

當設置偏向鎖時,Java 虛擬機須要將該epoch值複製到鎖對象的標記字段中。咱們規定,你加的偏向鎖的代數高,是能夠把代數低的PK下去的。


接下來我給你講的過程,你就知道爲何要這麼設計了。


咱們先從偏向鎖的撤銷講起。

當請求加鎖的線程和鎖對象標記字段保持的線程地址不匹配時(並且epoch即代數必須相等,如若不等,那麼當前線程能夠將該鎖重偏向至本身,由於新的epoch的代數確定要高於之前的代數),Java 虛擬機須要撤銷該偏向鎖。

這個撤銷過程很是麻煩,它要求持有偏向鎖的線程到達安全點,再將偏向鎖替換成輕量級鎖。

在宣佈某個類的偏向鎖失效時,Java 虛擬機實則將該類的epoch值加 1,表示以前那一代的偏向鎖已經失效。而新設置的偏向鎖則須要使用類中的最新epoch代數來加鎖。

爲了保證當前持有偏向鎖而且已加鎖的線程不至於所以丟鎖,Java虛擬機須要遍歷全部線程的Java棧,找出該類已加鎖的實例,而且將它們標記字段中的 epoch值加1。該操做須要全部線程處於安全點狀態。

因此有專家近年來提出,偏向鎖在鎖競爭激烈的狀況下,非但不能優化性能,反而可能傷害應用性能。

若是總撤銷數超過另外一個閾值(對應 Java 虛擬機參數-XX:BiasedLockingBulkRevokeThreshold,默認值爲 40),那麼 Java 虛擬機會認爲這個類已經再也不適合偏向鎖。

此時,Java 虛擬機會撤銷該類實例的偏向鎖,而且在以後的加鎖過程當中直接爲該類實例設置輕量級鎖。


END


歡迎長按下圖關注公衆號:石杉的架構筆記!

公衆號後臺回覆資料,獲取做者獨家祕製學習資料

石杉的架構筆記,BAT架構經驗傾囊相授

相關文章
相關標籤/搜索