Java作為主流的面向對象編程語言,提供了豐富的并發工具來幫助開發者解決多線程環境下的數據一致性問題。其中,內置的關鍵字"Synchronized"扮演了至關重要的角色,它能夠確保在同一時刻只有一個線程訪問特定代碼塊或方法,從而有效地防止數據競爭和保持內存可見性。
在傳統的Synchronized實現中,由于其采用的是重量級鎖機制,每次獲取和釋放鎖都涉及操作系統層面的線程調度,這無疑增加了線程上下文切換的開銷,尤其在高并發且鎖競爭較小的場景下,可能會導致不必要的性能損失。為此,從Java 6開始,JVM引入了鎖升級機制,這是一種動態調整鎖狀態的技術,旨在根據不同場景靈活運用不同級別的鎖,從而在保證并發安全性的同時,最大程度地提升程序的運行效率。
本文將深入探討"Synchronized"的鎖升級過程,詳細介紹從無鎖狀態到偏向鎖、輕量級鎖,直至重量級鎖的不同階段及其背后的原理。
在Java中,synchronized關鍵字是實現線程同步的關鍵機制之一,它用于確保多個線程在訪問共享資源時的正確性和一致性。synchronized鎖的基本思想是,當一個線程進入某個synchronized代碼塊或方法時,它必須首先獲取到該對象或類的鎖,然后才能執行相應的操作。如果其他線程試圖進入相同的synchronized區域,它們將被阻塞,直到鎖被釋放。
Java對象在內存中不僅包含類實例的字段,還包含一些元數據,這些元數據存儲在對象頭中。對象頭是Java對象的重要組成部分,它包含了關于對象的重要信息,如哈希碼、GC年齡以及鎖狀態等。其中,Mark Word是對象頭中的一個關鍵字段,它記錄了關于對象鎖狀態的信息。通過修改Mark Word的內容,JVM能夠實現對對象鎖的獲取和釋放。
synchronized鎖定的基本原理是通過對對象或類的監視器(Monitor)進行加鎖和解鎖操作來實現線程同步。當一個線程嘗試進入synchronized代碼塊或方法時,它會首先嘗試獲取對象或類的鎖。如果鎖已經被其他線程持有,則該線程將被阻塞,直到鎖被釋放。synchronized鎖的運作機制包括偏向鎖、輕量級鎖和重量級鎖三種狀態。偏向鎖適用于單線程訪問的情況,輕量級鎖適用于多線程競爭不激烈的情況,而重量級鎖則用于處理高競爭場景。通過這三種狀態的轉換,synchronized鎖能夠根據不同的并發場景動態調整鎖策略,以實現高效的線程同步。
關于synchronized的實現方式,原理介紹,請參考:美團一面:說說_synchronized_的實現原理?問麻了。。。。
鎖升級是指Java虛擬機(JVM)在并發環境下對synchronized關鍵字所使用的鎖機制進行動態調整的過程,從最初的無鎖狀態逐漸過渡到偏向鎖、輕量級鎖,直至最終的重量級鎖。這一過程旨在根據實際的并發狀況選擇最適合的鎖類型,以實現對共享資源的最佳保護和最有效的并發控制。
鎖升級的主要目的是為了提升并發性能,減少不必要的線程上下文切換和內存消耗。線程上下文切換是一個相對昂貴的操作,因為它涉及到保存當前線程的狀態、恢復另一個線程的狀態等一系列操作。通過優化鎖策略,JVM可以減少這種切換的頻率,從而提高系統的整體性能。
另外,鎖升級也有助于減少內存消耗。相較于重量級鎖需要創建額外的Monitor對象并在操作系統層面進行線程調度,偏向鎖和輕量級鎖在一定程度上降低了內存消耗,特別是對于大量短生命周期的鎖請求場景。
當我們使用synchronized時,Java虛擬機(JVM)會為每個被同步的對象維護一個鎖(或稱為監視器鎖)。這個鎖有四種狀態:從級別由低到高依次是:無鎖、偏向鎖,輕量級鎖,重量級鎖,用于控制多線程對共享資源的訪問。
四種鎖狀態的轉換
無鎖狀態是對象初始化后的默認鎖狀態,表示對象當前未被任何線程鎖定。在這種狀態下,對象頭的鎖標志位通常為空或特定的無鎖標識,表明對象不受任何同步控制,任何線程都能夠無障礙地訪問該對象。
無鎖的標志位為01,即如果是否偏向鎖標識為0時是無鎖狀態,為1時是偏向鎖。在這個狀態下,沒有線程擁有鎖,并且存儲了對象的hashcode、對象的分代年齡以及是否為偏向鎖的標志(0表示不是偏向鎖)。
當一個線程首次嘗試獲取鎖時,JVM會檢查這個鎖是否處于無鎖狀態。如果是,JVM會嘗試將鎖偏向給這個線程,也就是將鎖標記為偏向這個線程,并且將這個線程的ID記錄在鎖的標記中。這樣,當這個線程再次嘗試獲取鎖時,就可以避免一些昂貴的操作,因為JVM可以直接檢查鎖是否仍然偏向這個線程。
當一個線程首次成功獲取一個鎖時,鎖就進入了偏向鎖狀態。在偏向鎖狀態下,只有持有偏向鎖的線程才能再次獲取這個鎖,而不會引起競爭。如果其他線程嘗試獲取這個鎖,偏向鎖就會升級為輕量級鎖。
偏向鎖的標志位為01,即是否偏向鎖表標識位為1。與無鎖狀態的標志位相同,但存儲的內容有所不同。偏向鎖狀態下,會存儲偏向的線程ID、偏向時間戳、對象分代年齡以及是否偏向鎖的標志(1)。
偏向鎖是一種針對線程獨占鎖優化的機制,它適用于單一線程長時間、連續地訪問同一段同步代碼的情況。當某個線程首次獲得同步代碼塊的鎖后,Java虛擬機會在對象頭的Mark Word中記錄該線程的ID,形成偏向鎖。在此之后,該線程再次進入同步代碼塊時,無需執行CAS操作等復雜的同步動作,僅需確認Mark Word中的偏向線程ID是否為自己,便可迅速獲得鎖,從而極大地減少了獲取鎖的開銷,提升了并發性能。
在偏向鎖生效期間,除非有其他線程嘗試獲取該鎖,否則持有偏向鎖的線程不會主動釋放鎖。當出現鎖競爭時,原有的偏向鎖持有者會經歷撤銷過程。此過程發生在全局安全點,即在所有線程均停止執行字節碼的時刻,JVM會暫停當前持有偏向鎖的線程,檢查鎖對象的狀態。如果發現持有偏向鎖的線程不再活動或者鎖確實處于被爭奪狀態,則會撤銷偏向鎖,即將對象頭恢復為無鎖狀態(標志位為01)或直接升級為輕量級鎖(標志位調整為對應輕量級鎖的狀態)。
偏向鎖主要是為了解決在一個線程連續多次獲取同一鎖的情況,降低不必要的同步操作開銷。當首次獲取鎖的線程再次進入同步代碼塊時,會檢查對象頭中存儲的線程ID是否與當前線程一致。如果一致,則直接獲得鎖;如果不一致,則需要撤銷偏向鎖,重新進行鎖競爭,可能升級為輕量級鎖。
優點: 對于沒有或很少發生鎖競爭的場景,偏向鎖可以顯著減少鎖的獲取和釋放所帶來的性能損耗。
缺點:
當一個線程嘗試獲取一個已經被其他線程持有的偏向鎖時,偏向鎖會升級為輕量級鎖。輕量級鎖是一種用于處理線程之間輕量級競爭的機制。當一個線程嘗試獲取輕量級鎖時,它會先自旋一段時間,嘗試等待鎖被釋放。如果在這段時間內鎖被釋放了,那么這個線程就可以成功獲取鎖。如果自旋結束后鎖仍然被持有,那么這個線程就會嘗試將鎖升級為重量級鎖。
輕量級鎖的標識位為: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的釋放操作來喚醒等待隊列中的下一個線程。
? 被喚醒的線程將獲得鎖并繼續執行同步代碼,確保資源的順序訪問和線程安全
鎖的升級流程
鎖降級通常出現在使用讀寫鎖(如Java中的ReentrantReadWriteLock)的場景中。在多線程環境下,一個線程首先獲取到了寫鎖,那么在它持有寫鎖期間,任何其他線程都無法獲取讀鎖或寫鎖,確保了對該資源的獨占訪問權以進行修改。這個在持有寫鎖的同時,線程會嘗試獲取讀鎖。由于該線程已經持有寫鎖,所以它可以成功獲取讀鎖,而不會造成死鎖或其他同步問題。然后線程釋會放寫鎖,但仍持有讀鎖。此時,其他線程可以獲取讀鎖進行讀取操作,但無法獲取寫鎖進行寫入操作。
鎖降級的意義在于,線程在完成寫操作后,如果接下來的任務主要是讀取而不是繼續寫入,那么通過降級能夠允許其他讀線程同時訪問資源,提高了系統的并發性能,同時保證了數據一致性,因為所有讀線程看到的都是最近一次寫操作完成后的一致性視圖。鎖降級是針對讀寫鎖的一種高級使用方式,用于提升多讀少寫的并發場景性能。
鎖消除(Lock Elimination)是一種由編譯器或虛擬機在運行時進行的優化技術,其目的是去除那些不必要的鎖操作。當編譯器或JVM的即時編譯器(JIT Compiler)在分析代碼時發現某個鎖保護的變量并沒有發生實際的共享數據競爭,也就是說,該變量的生命周期僅限于方法內部,不會逃逸出該方法,那么這個鎖就可以安全地被消除掉。
例如,如果一段同步代碼塊中的變量只在棧上分配并且沒有其他線程可以直接訪問,那么即使對該變量進行了同步也不會帶來任何好處,反而增加了上下文切換和鎖獲取釋放的開銷。在這種情況下,JVM可以通過逃逸分析等手段確定該變量不存在共享狀態,進而消除對它的同步操作。
鎖消除則是編譯器和JVM層面的一種優化技術,用于消除不必要的同步,減少鎖帶來的性能損耗。
Synchronized鎖升級機制是Java虛擬機為優化多線程環境下同步操作性能而設計的一種動態調整策略。通過偏向鎖、輕量級鎖和重量級鎖之間的智能轉換,JVM可以根據實際的并發狀況在低競爭和高競爭場景下分別采取不同的鎖策略,從而有效減少線程上下文切換、內存占用以及CPU空轉等問題,提升系統的整體并發性能。
偏向鎖適用于單一線程反復訪問同一鎖的情況,輕量級鎖則在輕度競爭場景下通過CAS和自旋優化鎖的獲取和釋放,而重量級鎖雖然開銷較大,但在高強度競爭下提供了嚴格的互斥性和線程調度的公平性。
本文鏈接:http://www.tebozhan.com/showinfo-26-83989-0.html京東二面:Sychronized的鎖升級過程是怎樣的?
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: Stream.parallel():開啟并行流處理之旅
下一篇: RabbitMQ實現延遲隊列的技術探討