京東二面:Sychronized的鎖升級過程是怎樣的

码农Academy發表於2024-05-21

引言

Java作為主流的物件導向程式語言,提供了豐富的併發工具來幫助開發者解決多執行緒環境下的資料一致性問題。其中,內建的關鍵字"Synchronized"扮演了至關重要的角色,它能夠確保在同一時刻只有一個執行緒訪問特定程式碼塊或方法,從而有效地防止資料競爭和保持記憶體可見性。

在傳統的Synchronized實現中,由於其採用的是重量級鎖機制,每次獲取和釋放鎖都涉及作業系統層面的執行緒排程,這無疑增加了執行緒上下文切換的開銷,尤其在高併發且鎖競爭較小的場景下,可能會導致不必要的效能損失。為此,從Java 6開始,JVM引入了鎖升級機制,這是一種動態調整鎖狀態的技術,旨在根據不同場景靈活運用不同級別的鎖,從而在保證併發安全性的同時,最大程度地提升程式的執行效率。

關於Synchronized的實現原理,請參考:美團一面:說說synchronized的實現原理?問麻了。。。。

本文將深入探討"Synchronized"的鎖升級過程,詳細介紹從無鎖狀態到偏向鎖、輕量級鎖,直至重量級鎖的不同階段及其背後的原理。

Synchronized鎖的基礎概念

在Java中,synchronized關鍵字是實現執行緒同步的關鍵機制之一,它用於確保多個執行緒在訪問共享資源時的正確性和一致性。synchronized鎖的基本思想是,當一個執行緒進入某個synchronized程式碼塊或方法時,它必須首先獲取到該物件或類的鎖,然後才能執行相應的操作。如果其他執行緒試圖進入相同的synchronized區域,它們將被阻塞,直到鎖被釋放。

物件頭與Mark Word簡介

Java物件在記憶體中不僅包含類例項的欄位,還包含一些後設資料,這些後設資料儲存在物件頭中。物件頭是Java物件的重要組成部分,它包含了關於物件的重要資訊,如雜湊碼、GC年齡以及鎖狀態等。其中,Mark Word是物件頭中的一個關鍵欄位,它記錄了關於物件鎖狀態的資訊。透過修改Mark Word的內容,JVM能夠實現對物件鎖的獲取和釋放。

Synchronized鎖定的基本原理與運作機制概述

synchronized鎖定的基本原理是透過對物件或類的監視器(Monitor)進行加鎖和解鎖操作來實現執行緒同步。當一個執行緒嘗試進入synchronized程式碼塊或方法時,它會首先嚐試獲取物件或類的鎖。如果鎖已經被其他執行緒持有,則該執行緒將被阻塞,直到鎖被釋放。synchronized鎖的運作機制包括偏向鎖、輕量級鎖和重量級鎖三種狀態。偏向鎖適用於單執行緒訪問的情況,輕量級鎖適用於多執行緒競爭不激烈的情況,而重量級鎖則用於處理高競爭場景。透過這三種狀態的轉換,synchronized鎖能夠根據不同的併發場景動態調整鎖策略,以實現高效的執行緒同步。

關於synchronized的實現方式,原理介紹,請參考:美團一面:說說synchronized的實現原理?問麻了。。。。

鎖升級的概念

鎖升級是指Java虛擬機器(JVM)在併發環境下對synchronized關鍵字所使用的鎖機制進行動態調整的過程,從最初的無鎖狀態逐漸過渡到偏向鎖、輕量級鎖,直至最終的重量級鎖。這一過程旨在根據實際的併發狀況選擇最適合的鎖型別,以實現對共享資源的最佳保護和最有效的併發控制。

鎖升級的主要目的是為了提升併發效能,減少不必要的執行緒上下文切換和記憶體消耗。執行緒上下文切換是一個相對昂貴的操作,因為它涉及到儲存當前執行緒的狀態、恢復另一個執行緒的狀態等一系列操作。透過最佳化鎖策略,JVM可以減少這種切換的頻率,從而提高系統的整體效能。

另外,鎖升級也有助於減少記憶體消耗。相較於重量級鎖需要建立額外的Monitor物件並在作業系統層面進行執行緒排程,偏向鎖和輕量級鎖在一定程度上降低了記憶體消耗,特別是對於大量短生命週期的鎖請求場景。

Synchronized鎖的四種狀態詳解

當我們使用synchronized時,Java虛擬機器(JVM)會為每個被同步的物件維護一個鎖(或稱為監視器鎖)。這個鎖有四種狀態:從級別由低到高依次是:無鎖、偏向鎖,輕量級鎖,重量級鎖,用於控制多執行緒對共享資源的訪問。

image.png

無鎖

無鎖狀態是物件初始化後的預設鎖狀態,表示物件當前未被任何執行緒鎖定。在這種狀態下,物件頭的鎖標誌位通常為空或特定的無鎖標識,表明物件不受任何同步控制,任何執行緒都能夠無障礙地訪問該物件。

無鎖的標誌位為01,即如果是否偏向鎖標識為0時是無鎖狀態,為1時是偏向鎖。在這個狀態下,沒有執行緒擁有鎖,並且儲存了物件的hashcode、物件的分代年齡以及是否為偏向鎖的標誌(0表示不是偏向鎖)。

當一個執行緒首次嘗試獲取鎖時,JVM會檢查這個鎖是否處於無鎖狀態。如果是,JVM會嘗試將鎖偏向給這個執行緒,也就是將鎖標記為偏向這個執行緒,並且將這個執行緒的ID記錄在鎖的標記中。這樣,當這個執行緒再次嘗試獲取鎖時,就可以避免一些昂貴的操作,因為JVM可以直接檢查鎖是否仍然偏向這個執行緒。

偏向鎖

當一個執行緒首次成功獲取一個鎖時,鎖就進入了偏向鎖狀態。在偏向鎖狀態下,只有持有偏向鎖的執行緒才能再次獲取這個鎖,而不會引起競爭。如果其他執行緒嘗試獲取這個鎖,偏向鎖就會升級為輕量級鎖。

偏向鎖的標誌位為01,即是否偏向鎖表標識位為1。與無鎖狀態的標誌位相同,但儲存的內容有所不同。偏向鎖狀態下,會儲存偏向的執行緒ID、偏向時間戳、物件分代年齡以及是否偏向鎖的標誌(1)。

偏向鎖是一種針對執行緒獨佔鎖最佳化的機制,它適用於單一執行緒長時間、連續地訪問同一段同步程式碼的情況。當某個執行緒首次獲得同步程式碼塊的鎖後,Java虛擬機器會在物件頭的Mark Word中記錄該執行緒的ID,形成偏向鎖。在此之後,該執行緒再次進入同步程式碼塊時,無需執行CAS操作等複雜的同步動作,僅需確認Mark Word中的偏向執行緒ID是否為自己,便可迅速獲得鎖,從而極大地減少了獲取鎖的開銷,提升了併發效能。

在偏向鎖生效期間,除非有其他執行緒嘗試獲取該鎖,否則持有偏向鎖的執行緒不會主動釋放鎖。當出現鎖競爭時,原有的偏向鎖持有者會經歷撤銷過程。此過程發生在全域性安全點,即在所有執行緒均停止執行位元組碼的時刻,JVM會暫停當前持有偏向鎖的執行緒,檢查鎖物件的狀態。如果發現持有偏向鎖的執行緒不再活動或者鎖確實處於被爭奪狀態,則會撤銷偏向鎖,即將物件頭恢復為無鎖狀態(標誌位為01)或直接升級為輕量級鎖(標誌位調整為對應輕量級鎖的狀態)。

偏向鎖主要是為了解決在一個執行緒連續多次獲取同一鎖的情況,降低不必要的同步操作開銷。當首次獲取鎖的執行緒再次進入同步程式碼塊時,會檢查物件頭中儲存的執行緒ID是否與當前執行緒一致。如果一致,則直接獲得鎖;如果不一致,則需要撤銷偏向鎖,重新進行鎖競爭,可能升級為輕量級鎖。

優點
對於沒有或很少發生鎖競爭的場景,偏向鎖可以顯著減少鎖的獲取和釋放所帶來的效能損耗。

缺點

  • 額外儲存空間:偏向鎖會在物件頭中儲存一個偏向執行緒ID等相關資訊,這部分額外的空間開銷雖然較小,但在大規模併發場景下,累積起來也可能成為可觀的成本。

  • 鎖升級開銷:當一個偏向鎖的物件被其他執行緒訪問時,需要進行撤銷(revoke)操作,將偏向鎖升級為輕量級鎖,甚至在更高競爭情況下升級為重量級鎖。這個升級過程涉及到CAS操作以及可能的執行緒掛起和喚醒,會帶來一定的效能開銷。

  • 適用場景有限:偏向鎖最適合於絕大部分時間只有一個執行緒訪問物件的場景,這樣的情況下,偏向鎖的開銷可以降到最低,有利於提高程式效能。但如果併發程度較高,或者執行緒切換頻繁,偏向鎖就可能不如輕量級鎖或重量級鎖高效。

輕量級鎖

當一個執行緒嘗試獲取一個已經被其他執行緒持有的偏向鎖時,偏向鎖會升級為輕量級鎖。輕量級鎖是一種用於處理執行緒之間輕量級競爭的機制。當一個執行緒嘗試獲取輕量級鎖時,它會先自旋一段時間,嘗試等待鎖被釋放。如果在這段時間內鎖被釋放了,那麼這個執行緒就可以成功獲取鎖。如果自旋結束後鎖仍然被持有,那麼這個執行緒就會嘗試將鎖升級為重量級鎖。

輕量級鎖的標識位為:00。當鎖從偏向鎖升級為輕量級鎖時,標誌位會變為00。在輕量級鎖狀態下,多個執行緒可能會嘗試獲取鎖,透過自旋來等待鎖被釋放。

輕量級鎖利用CAS操作嘗試將物件頭的Mark Word替換為指向執行緒棧中鎖記錄的指標,如果CAS操作成功,則表示執行緒成功獲取鎖。獲取鎖失敗的執行緒會進入自旋狀態,不斷迴圈嘗試獲取鎖,直到獲取成功或升級為重量級鎖。在自旋期間,執行緒不會立即進入阻塞狀態,而是不斷迴圈檢查鎖是否可用。這種機制可以減少執行緒上下文切換的開銷,但如果自旋次數過多或者競爭加劇,自旋就會失去意義,JVM會選擇升級為重量級鎖。

優點

  • 低開銷:輕量級鎖透過CAS操作嘗試獲取鎖,避免了重量級鎖中涉及的執行緒掛起和恢復等高昂開銷。
  • 快速響應:在無鎖競爭或者鎖競爭不激烈的情況下,輕量級鎖使得執行緒可以迅速獲取鎖並執行同步程式碼塊。

缺點

  • 自旋消耗:當鎖競爭激烈時,執行緒可能會長時間自旋等待鎖,這會消耗CPU資源,導致效能下降。
  • 升級開銷:如果自旋等待超過一定閾值或者鎖競爭加劇,輕量級鎖會升級為重量級鎖,這個升級過程本身也有一定的開銷。

重量級鎖

當輕量級鎖的自旋嘗試達到一定閾值,或者檢測到多個執行緒競爭激烈時,JVM會將輕量級鎖升級為重量級鎖。升級過程中,會取消當前執行緒的自旋操作,並在物件頭中設定重量級鎖標誌。

重量級鎖的標識位為:10。當鎖從輕量級鎖升級為重量級鎖時,標誌位會變為10。在重量級鎖狀態下,執行緒在獲取鎖時會阻塞,直到持有鎖的執行緒釋放鎖。

在重量級鎖狀態下,執行緒在獲取鎖失敗時會被作業系統掛起,放入到該物件關聯的監視器(Monitor)的等待佇列中,由作業系統進行執行緒排程,當鎖被釋放時,作業系統會選擇合適的執行緒將其喚醒並授予鎖。

儘管重量級鎖的開銷較大,涉及到執行緒上下文切換和核心態使用者態的切換等,但它在高競爭場景下能提供穩定的互斥性和公平性,確保資料的一致性和執行緒的安全執行。因此,即使效能損耗較高,也是在特定情況下必要的權衡措施。

優點

  • 強一致性:重量級鎖提供了最強的執行緒安全性,確保在多執行緒環境下資料的完整性和一致性。
  • 簡單易用synchronized關鍵字的使用簡潔明瞭,不易出錯。

缺點

  • 效能開銷大:獲取和釋放重量級鎖時需要作業系統介入,可能涉及執行緒的掛起和喚醒,造成上下文切換,這對於頻繁鎖競爭的場景來說效能代價較高。
  • 延遲較高:執行緒獲取不到鎖時會被阻塞,導致等待時間增加,進而影響系統響應速度。

以上四種鎖狀態優缺點對比總結如下:

型別 優點 缺點 使用場景
偏向鎖 快速:無須執行緒上下文切換,適合單一執行緒多次重複獲取同一執行緒鎖的場景
低開銷:只需要檢查物件頭標記
不適合多執行緒競爭的場景
競爭時需要撤銷偏向鎖,有一定開銷
大多數時候只有一執行緒訪問同步程式碼塊,很少出現鎖競爭的情況
輕量級鎖 較快:透過CAS操作和自旋避免了執行緒的阻塞與喚醒,減少了執行緒上下文切換
適用於鎖競爭不激烈的場景
自旋可能導致CPU空耗,在高競爭下,大量的執行緒自旋會增加系統負擔。
無法保證絕對的公平性
短時間的同步程式碼塊,且鎖競爭不激烈,期望快速重入和釋放
重量級鎖 穩定可靠:嚴格保證互斥性和公平性
能夠有效應對高度競爭的鎖場景
開銷大:涉及到執行緒上下文切換,效能較低
阻塞執行緒可能導致響應時間變長
高併發、高競爭的場景,需要保證資料一致性,且執行緒等待鎖的時間較長或不可預知

關於Java中鎖的分類,以及各種所得介紹,請參考:阿里二面:Java中_鎖的分類_有哪些?你能說全嗎?

關於Java中如何定位以及避免死鎖,請參考:阿里二面:如何定位&避免_死鎖_?連著兩個面試問到了!

鎖升級的具體步驟與流程

1.無鎖到偏向鎖的升級流程:

  • 當執行緒首次嘗試獲取物件鎖時,JVM首先檢查物件是否處於無鎖狀態。
  • 若處於無鎖狀態,JVM則立即將其標記為偏向鎖,並記錄下當前執行緒的ID。
  • 這一過程透過CAS操作實現,確保執行緒安全地更新物件頭的Mark Word為偏向鎖狀態,並儲存偏向執行緒的ID。
  • 一旦設定成功,執行緒便可無阻礙地進入同步程式碼塊,後續再次獲取該鎖時僅需驗證是否仍偏向當前執行緒,無需額外同步操作

而對於偏向鎖的釋放機制:

  • 當持有偏向鎖的執行緒正常退出同步程式碼塊時,JVM僅簡單地更新物件頭的訪問計數等相關資訊。
  • 由於偏向鎖的設計初衷是最佳化同一執行緒對鎖的反覆獲取,因此它並不會立即釋放偏向關係,而是假設下一次仍由同一執行緒獲取鎖。

2. 偏向鎖到輕量級鎖的升級流程:

  • 當第二個執行緒嘗試獲取已被偏向的鎖時,它會首先校驗物件頭是否指向當前執行緒的ID。
  • 若校驗失敗,表明鎖已偏向其他執行緒,此時需要撤銷偏向鎖。
  • 撤銷後,物件會回到無鎖狀態或過渡至輕量級鎖狀態。
  • 接著,新執行緒會嘗試在其棧幀中建立鎖記錄,並使用CAS操作將物件頭的Mark Word替換為指向該鎖記錄的指標。
  • 若CAS操作成功,執行緒即獲得輕量級鎖;若失敗,則進入自旋狀態,迴圈嘗試獲取鎖。

對於輕量級鎖的釋放機制:

  • 持有輕量級鎖的執行緒在退出同步程式碼塊時,會嘗試透過CAS操作將物件頭恢復為原始狀態,即撤銷鎖記錄指標的替換。
  • 若CAS操作成功,則輕量級鎖被順利釋放;否則,可能需要進一步的鎖升級或處理。

3. 輕量級鎖到重量級鎖的升級流程:

  • 當輕量級鎖的持有執行緒退出同步程式碼塊並釋放鎖時,它會嘗試將物件頭恢復到無鎖或偏向鎖狀態。
  • 若存在多個執行緒競爭鎖資源,輕量級鎖的釋放可能導致自旋執行緒長時間無法獲取鎖。
  • JVM會綜合考量自旋次數、競爭激烈程度以及系統負載等因素,決策是否將輕量級鎖升級為重量級鎖。
  • 一旦升級為重量級鎖,原持有執行緒必須完成鎖的釋放。新來的執行緒將被阻塞,並被加入物件的監視器(Monitor)等待佇列,由作業系統負責執行緒的排程管理。

對於釋放重量級鎖:

  • 持有重量級鎖的執行緒在退出同步程式碼塊時,會透過呼叫Monitor的釋放操作來喚醒等待佇列中的下一個執行緒。
  • 被喚醒的執行緒將獲得鎖並繼續執行同步程式碼,確保資源的順序訪問和執行緒安全

image.png

鎖降級與鎖消除

鎖降級

鎖降級通常出現在使用讀寫鎖(如Java中的ReentrantReadWriteLock)的場景中。在多執行緒環境下,一個執行緒首先獲取到了寫鎖,那麼在它持有寫鎖期間,任何其他執行緒都無法獲取讀鎖或寫鎖,確保了對該資源的獨佔訪問權以進行修改。這個在持有寫鎖的同時,執行緒會嘗試獲取讀鎖。由於該執行緒已經持有寫鎖,所以它可以成功獲取讀鎖,而不會造成死鎖或其他同步問題。然後執行緒釋會放寫鎖,但仍持有讀鎖。此時,其他執行緒可以獲取讀鎖進行讀取操作,但無法獲取寫鎖進行寫入操作。

鎖降級的意義在於,執行緒在完成寫操作後,如果接下來的任務主要是讀取而不是繼續寫入,那麼透過降級能夠允許其他讀執行緒同時訪問資源,提高了系統的併發效能,同時保證了資料一致性,因為所有讀執行緒看到的都是最近一次寫操作完成後的一致性檢視。鎖降級是針對讀寫鎖的一種高階使用方式,用於提升多讀少寫的併發場景效能。

鎖消除

鎖消除(Lock Elimination)是一種由編譯器或虛擬機器在執行時進行的最佳化技術,其目的是去除那些不必要的鎖操作。當編譯器或JVM的即時編譯器(JIT Compiler)在分析程式碼時發現某個鎖保護的變數並沒有發生實際的共享資料競爭,也就是說,該變數的生命週期僅限於方法內部,不會逃逸出該方法,那麼這個鎖就可以安全地被消除掉。

例如,如果一段同步程式碼塊中的變數只在棧上分配並且沒有其他執行緒可以直接訪問,那麼即使對該變數進行了同步也不會帶來任何好處,反而增加了上下文切換和鎖獲取釋放的開銷。在這種情況下,JVM可以透過逃逸分析等手段確定該變數不存在共享狀態,進而消除對它的同步操作。

鎖消除則是編譯器和JVM層面的一種最佳化技術,用於消除不必要的同步,減少鎖帶來的效能損耗。

總結

Synchronized鎖升級機制是Java虛擬機器為最佳化多執行緒環境下同步操作效能而設計的一種動態調整策略。透過偏向鎖、輕量級鎖和重量級鎖之間的智慧轉換,JVM可以根據實際的併發狀況在低競爭和高競爭場景下分別採取不同的鎖策略,從而有效減少執行緒上下文切換、記憶體佔用以及CPU空轉等問題,提升系統的整體併發效能。

偏向鎖適用於單一執行緒反覆訪問同一鎖的情況,輕量級鎖則在輕度競爭場景下透過CAS和自旋最佳化鎖的獲取和釋放,而重量級鎖雖然開銷較大,但在高強度競爭下提供了嚴格的互斥性和執行緒排程的公平性。

本文已收錄於我的個人部落格:碼農Academy的部落格,專注分享Java技術乾貨,包括Java基礎、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中介軟體、架構設計、面試題、程式設計師攻略等

相關文章