Hotspot 三種鎖實現總結

 目錄

一、BasicObjectLock / BasicLock 

二、偏向鎖

三、輕量級鎖

四、重量級鎖

六、整體加鎖流程

七、整體解鎖流程


      Java6對synchronized關鍵字進行了大幅度的優化,引入偏向鎖和輕量級鎖的概念,大幅減少了獲得鎖和釋放鎖的性能損耗。從Java6開始,加上原有的重量級鎖(又叫監視器鎖),Java中的對象鎖一共有4種狀態,分別是:無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態。對象鎖只能按照上述的順序進行升級(又稱鎖膨脹)操作,鎖只要升級之後,就不能降級。本篇博客基於前面幾篇的代碼分析總結一下偏向鎖、輕量級鎖和重量級鎖的實現及其鎖膨脹的流程。

一、BasicObjectLock / BasicLock 

       這兩個都是加鎖時使用的數據結構,其定義如下:

其中lock屬性的地址用於實現輕量級鎖,即所謂的Thread ID;obj屬性用於保存關聯的鎖對象oop;displaced_header屬性用於保存鎖對象oop的原始對象頭,即無鎖狀態下的對象頭,但是在synchronized嵌套的情形下displaced_header爲NULL,因爲外層synchronized對應的BasicObjectLock已經保存了原始對象頭了,此處不需要再保存;另外,如果某個輕量級鎖膨脹成重量級鎖了,則displaced_header會被置爲unused_mark,因爲重量級鎖本身會保存鎖對象oop的原始對象頭。synchronized嵌套的示例如下:

    準備加鎖前會從當前方法對應的調用棧幀中查找一個空閒的BasicObjectLock,obj屬性爲NULL的就表示空閒的,查找時先從低地址即從最近才分配的BasicObjectLock開始遍歷,如果找到一個obj屬性跟目標對象一致的就終止遍歷,BasicObjectLock就是外層synchronized對應的BasicObjectLock,如果沒有找到obj屬性跟目標對象一致的就會把所有的BasicObjectLock都遍歷一遍,取地址最高的一個空閒BasicObjectLock,提高BasicObjectLock的使用率。

    準備解鎖前會從當前方法對應的調用棧幀中找到一個obj屬性與目標對象一致的BasicObjectLock,遍歷時從低地址即從最近才分配的BasicObjectLock開始遍歷,找到了就立即返回,如果沒有找到則拋出異常IllegalMonitorStateException。

    注意上述的加鎖解鎖都是針對於synchronized關鍵字,如果是通過jni_MonitorEnter/jni_MonitorExit方法,Unsafe類的monitorenter/monitorexit方法加鎖解鎖則不需要分配BasicObjectLock,而是直接分配並使用重量級鎖。

二、偏向鎖

    適用場景:只有一個線程進入同步代碼塊

    定義:偏向鎖顧名思義會偏向於一個線程,如果有其他線程來搶佔這個偏向鎖則會導致偏向鎖被撤銷,恢復成無鎖狀態或者直接膨脹成輕量級鎖。當偏向鎖沒有被某個線程佔用,即對象頭中用於保存線程指針的位都是0的時候,這個偏向鎖就是匿名偏向鎖。偏向鎖可以通過-XX:-UseBiasedLocking來關閉,該參數默認爲true。

    初始化:偏向鎖默認是延時初始化的,延遲的時間通過參數BiasedLockingStartupDelay控制,默認是4000ms。初始化是在安全點下通過VMThread完成的,初始化時會把由SystemDictionary維護的所有已加載類的Klass的prototype_header修改成匿名偏向鎖對象頭,並把_biased_locking_enabled靜態屬性置爲true,後續加載新的Klass時發現該屬性爲true,會將Klass的prototype_header修改成匿名偏向鎖對象頭。當創建某個Klass的oop時,會利用Klass的prototype_header來初始化該oop的對象頭,即偏向鎖初始化完成後,後續所有創建的oop的初始對象頭都是匿名偏向鎖的,在此之前創建的oop初始對象頭都是無鎖狀態的。

   加鎖流程:

偏向鎖的加鎖實現就是MacroAssembler::biased_locking_enter方法,其中slow_case就是調用InterpreterRuntime::monitorenter方法,該方法會判斷目標偏向鎖是否正在使用,如果是則將其直接膨脹成輕量級鎖,如果不是則恢復成無鎖狀態;其中搶佔偏向鎖就是通過CAS原子的將當前線程對象的指針通過或運算寫入鎖對象的對象頭中,寫入成功視爲搶佔成功。

   解鎖:解鎖的實現在MacroAssembler::biased_locking_exit方法中,其實現非常簡單,只是簡單的判斷下目標對象的對象頭是否還是偏向鎖,如果是則解鎖完成,注意此時並不會將裏面的線程指針去掉,這樣做是爲了該線程下一次加鎖的時候可以很快的完成加鎖;如果不是,則退出該方法。

   撤銷:撤銷偏向鎖就是將鎖對象oop的對象頭恢復成無鎖狀態或者膨脹成輕量級鎖狀態,執行撤銷動作的前提是鎖對象oop的對象頭處於偏向鎖狀態。具體而言有以下幾種情形:

  1. 執行Object類的hashcode方法,會將其恢復成無鎖狀態。
  2. 執行Object類的wait/notify/notifyall方法,會將其恢復成無鎖狀態,直接膨脹成重量級鎖。
  3. 執行jni_MonitorEnter或者jni_MonitorExit方法,會將其恢復成無鎖狀態,直接膨脹成重量級鎖。
  4. 執行Unsafe類的monitorenter/trymonitorenter/monitorexit方法,會將其恢復成無鎖狀態,直接膨脹成重量級鎖。
  5. 嘗試獲取某個偏向鎖,如果該偏向鎖被某個線程佔用了,但是沒有關聯的BasicObjectLock,即實際佔用該偏向鎖的方法已經退出了,則會將其恢復成無鎖狀態,然後膨脹成輕量級鎖,但是在撤銷一定次數後觸發批量重偏向(rebasic)的情形下也可能重新獲取該偏向鎖。如果該偏向鎖正在被某個方法所使用,即存在對應的BasicObjectLock,則直接將該偏向鎖膨脹成輕量級鎖。

注意偏向鎖的撤銷大部分情形下都是需要在安全點下執行,因爲需要遍歷其他線程的所有調用棧幀,判斷是否存在與之關聯的BasicObjectLock。在以下情形不需要在安全點下執行:

  1. 目標對象的對象頭是匿名偏向鎖狀態
  2. 目標對象的Klass的prototype_header變成無鎖狀態
  3. 目標對象的Klass的prototype_header中的epoch值和目標對象對象頭中的epoch值不一樣
  4. 目標對象的偏向鎖由當前線程持有

    批量重偏向: 因爲偏向鎖的撤銷大部分情形都是在安全點下執行的,安全點同步會導致系統停頓,整體性能損耗較高,所以當個某一類Klass對應的鎖對象oop被累計撤銷一定次數後就會觸發批量重偏向,這個次數通過BiasedLockingBulkRebiasThreshold參數控制,默認是20。批量重偏向會先將Klass的prototype_header中的epoch值加1,然後遍歷所有JavaThread的所有棧幀,遍歷每個棧幀中包含的BasicObjectLock,如果其關聯的鎖對象oop是該Klass,則增加該oop的對象頭中的epoch值,遍歷完成後將觸發批量重偏向的這個鎖對象oop重新偏向給當前線程。注意此時不在棧幀中的即未被實際佔用的鎖對象的oop的epoch值就不會改變,重新獲取該鎖對象oop的偏向鎖時因爲epoch值不一致就可以重新被其他線程搶佔,即提高偏向鎖oop的使用率。

    批量撤銷:觸發批量重偏向後如果頻繁撤銷,在一定的時間段內超過一定次數則觸發批量撤銷,時間段通過參數BiasedLockingDecayTime控制,默認是25000ms,次數通過BiasedLockingBulkRevokeThreshold參數控制,默認是40,即25s內該Klass的鎖對象oop撤銷偏向鎖累計超過40次後就觸發批量撤銷。批量撤銷會將將Klass的prototype_header恢復成無鎖狀態,即新創建的oop的對象頭都是無鎖狀態,不支持使用偏向鎖了,然後遍歷所有JavaThread的所有棧幀,遍歷每個棧幀中包含的BasicObjectLock,如果其關聯的鎖對象oop是該Klass,則將該鎖對象oop的對象頭膨脹成輕量級鎖。

    上述批量動作的觸發邏輯都在BiasedLocking::update_heuristics方法中,具體的執行邏輯在BiasedLocking::bulk_revoke_or_rebias_at_safepoint方法中,跟單個撤銷不同,批量動作必須在安全點下執行,因爲需要遍歷所有Java線程的所有棧幀。

三、輕量級鎖

     適用場景:多個線程交替進入同步代碼塊

     定義:輕量級鎖是相對於重量級鎖的,即加鎖的成本更低,性能更好,只需要將lock屬性地址通過CAS寫入對象頭即視爲加鎖成功,因爲BasicLock只有一個8字節屬性,所以lock屬性的地址是8字節對齊的,其最後3位剛好是000。輕量級鎖是必須開啓的,沒有參數將其關閉。當存在多個線程搶佔輕量級鎖的時候,只有一個能夠搶佔成功,獲取輕量級鎖恢復正常執行,其他線程都會嘗試將該輕量級鎖膨脹成重量級鎖,也只有一個線程完成鎖膨脹。

     加鎖流程:

輕量級鎖的加鎖流程在TemplateTable::monitorenter 和InterpreterRuntime::monitorenter方法中都有,其邏輯是一樣的,前者是monitorenter字節碼指令的實現,後者是獲取偏向鎖或者輕量級鎖失敗後執行的邏輯。注意將lock屬性寫入對象頭是CAS原子動作,只有一個線程能夠寫入成功,寫入成功視爲搶佔成功。

     解鎖流程:

   

 displaced_header爲NULL時就是synchronized嵌套的情形,此時解鎖不需要恢復鎖對象的對象頭,最外層synchronized解鎖時會恢復對象頭。將displaced_header中的對象頭寫入鎖對象動作是CAS的,如果成功,解鎖成功,如果失敗,說明某個線程已經將該輕量級鎖膨脹成重量級鎖了,需要獲取對應的重量級鎖,完成解鎖動作。對比偏向鎖的解鎖實現可知,輕量級鎖支持多個線程佔有,但是必須是交替的,不能是同時的。另外很多博客說輕量級鎖膨脹成重量級鎖前有一個自旋等待的動作,這其實是錯誤的,輕量級鎖的實現只使用了BasicObjectLock一個數據結構,無法支持自旋等待,因爲沒有地方記錄自旋等待的次數,倒是重量級在採用互斥量阻塞當前線程前會先嚐試自旋等待一段時間,重量級鎖有單獨的數據結構可以支持複雜的自旋邏輯。

四、重量級鎖

使用場景:多個線程同時進入同步代碼塊

定義:重量級鎖就是利用底層操作系統mutex相關API和線程自旋實現的鎖,因爲mutex API會讓線程休眠,導致線程的上下文切換,性能損耗較大,所以稱之爲重量級鎖,也叫監視器鎖。重量級鎖有專門的數據結構ObjectMonitor,不依賴於BasicObjectLock。

鎖膨脹流程:

INFLATING是一個非常短暫的中間狀態,只存在於輕量級鎖膨脹成重量級鎖的過程中,主要爲了防止對象的hashCode出現閃爍。上述流程中有兩處CAS原子修改動作,修改失敗就表示有其他線程成功的修改了該對象頭,需要重新開始下一次循環,重新獲取新的對象頭狀態。

加鎖流程:

owner屬性表示佔用當前重量級鎖的線程,如果owner屬性就是當前線程說明是嵌套加鎖,需要將記錄嵌套加鎖的屬性加1,如果owner屬性位於當前線程的調用棧幀中,說明該重量級鎖是由輕量級鎖膨脹而來,原來佔用輕量級鎖的線程依然佔用該鎖,此處再調用enter則說明是第一次嵌套加鎖,將記錄嵌套加鎖的屬性置爲1,同時將owner屬性置爲當前線程。

自旋搶佔時自旋的次數可以固定,也可以自適應的動態調整,默認是後者;自旋的過程中會檢查是否進入了安全點同步,如果是則退出自旋。所謂的park是利用底層操作系統的mutex相關API,讓當前線程休眠,一旦佔有鎖的線程釋放了鎖就會喚醒休眠的線程,然後嘗試佔用該鎖,佔有失敗則嘗試自旋搶佔,還是失敗則繼續park。

在修改線程狀態爲阻塞和恢復線程原來的運行狀態時都要檢查是否進入安全點同步,如果是則會阻塞當前線程完成狀態切換,即強制當前線程進入安全點等待。

wait流程:

注意線程被喚醒有幾種原因,wait方法設置的等待時間超時了,當前線程通過Thread.interrupt()方法被中斷了,持有鎖的線程釋放鎖時通過unpark喚醒當前線程。線程只是被喚醒了,不再處於休眠狀態,然後一樣調用enter方法搶佔鎖。 

notify流程:

調用enter方法獲取鎖時,如果自旋搶佔鎖失敗,同樣會創建一個ObjectWaiter並不將其添加到cxq鏈表中然後park,等待鎖釋放時將其喚醒,因此上述加入cxq鏈表的動作相當於調用了一次enter方法,比起直接unpark可以提高搶佔鎖的效率。另外WaitSet鏈表的頭元素是最早加入到鏈表中的線程,因此notify優先“喚醒”的是最早調用wait方法的線程。notifyAll和notify的核心流程是一樣的,就多了一個for循環會以同樣的方式處理所有等待的線程。

解鎖流程:

先釋放鎖再判斷鏈表是否爲空可保證正在自旋搶佔鎖的線程相對於處於休眠狀態的等待線程優先獲取鎖,EntryList初始化時就是空的,因此對於從cxq鏈表中轉移到EntryList鏈表中的元素,優先處理cxq鏈表頭,即最近纔等待的線程優先獲取鎖。

六、整體加鎖流程

    加鎖流程如下:

補充說明如下:

  1. 注意如果當前調用棧幀中存在多個空閒的BasicObjectLock,則取地址最高的一個;如果當前調用棧幀中已經存在obj屬性跟目標對象一樣的BasicObjectLock,則取該BasicObjectLock後面的空閒BasicObjectLock,如果沒有則重新分配一個,這樣做是爲了在synchronized嵌套使用的情形下解鎖時能夠很快的找到對應的BasicObjectLock。
  2. prototype_header只在安全點下通過bulk_revoke_or_rebias_at_safepoint方法更改,發生更改時會將所有線程的所有棧幀中包含屬於同一Klass的對象鎖oop的對象頭做相同的變更,如果某個對象鎖是偏向鎖但是與prototype_header不一致,則說明當時不在棧幀中,即沒有實際被佔用,因此可以直接搶佔該偏向鎖。
  3. 一個支持偏向鎖的Klass對應的實例在創建時是匿名偏向鎖狀態,即對象頭中的線程指針爲NULL,一旦該偏向鎖被某個線程獲取成功後就會將線程指針寫入對象頭,且在解鎖的時候也不會清除裏面的線程指針。再次獲取該偏向鎖時需要判斷對象頭中包含的線程指針對應的線程是否存在,如果存在且依然存在關聯的BasicObjectLock,說明該線程依然佔用該偏向鎖,需要將該偏向鎖膨脹成輕量級鎖,否則需要將偏向鎖撤銷恢復成無鎖狀態,然後獲取輕量級鎖。所有搶佔偏向鎖失敗的線程都會通過該邏輯膨脹成輕量級鎖。
  4. 將lock屬性寫入對象頭是一個原子動作,通過lock指令前綴實現,即如果存在多個線程同時搶佔輕量級鎖,只能有一個線程搶佔成功,其他線程執行時發現對象頭已經不是無鎖狀態了,只能將輕量級鎖膨脹成重量級鎖,等待獲取重量級鎖。
  5. 如果不是當前線程持有的偏向鎖,且prototype_header未發生改變,則可能是其他線程佔用了偏向鎖或者該對象的偏向鎖未被佔用(對象頭中包含的線程指針爲NULL,又稱匿名偏向鎖),如果是後者則嘗試搶佔該偏向鎖,如果搶佔失敗則撤銷偏向鎖觸發鎖膨脹

七、整體解鎖流程

    主要流程如下:

補充說明如下:

  1. 偏向鎖解鎖不需要恢復對象頭,也不需要去掉對象頭中包含的線程指針,但是輕量級鎖和重量級鎖解鎖時需要將目標對象的對象頭恢復成無鎖狀態
  2. 嵌套情形加鎖時displaced_header就是NULL,因爲外層的synchronized對應的BasicObjectLock已經保存了對象的原始對象頭了,內層synchronized對應的BasicObjectLock不需要再保存了,此時解鎖不需要恢復對象的原始對象頭,外層synchronized解鎖時會恢復原始對象頭

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章