京東二面:Sychronized的鎖升級過程是怎樣的?
引言
Java作為主流的面向?qū)ο缶幊陶Z言,提供了豐富的并發(fā)工具來幫助開發(fā)者解決多線程環(huán)境下的數(shù)據(jù)一致性問題。其中,內(nèi)置的關(guān)鍵字"Synchronized"扮演了至關(guān)重要的角色,它能夠確保在同一時(shí)刻只有一個(gè)線程訪問特定代碼塊或方法,從而有效地防止數(shù)據(jù)競爭和保持內(nèi)存可見性。
在傳統(tǒng)的Synchronized實(shí)現(xiàn)中,由于其采用的是重量級鎖機(jī)制,每次獲取和釋放鎖都涉及操作系統(tǒng)層面的線程調(diào)度,這無疑增加了線程上下文切換的開銷,尤其在高并發(fā)且鎖競爭較小的場景下,可能會導(dǎo)致不必要的性能損失。為此,從Java 6開始,JVM引入了鎖升級機(jī)制,這是一種動態(tài)調(diào)整鎖狀態(tài)的技術(shù),旨在根據(jù)不同場景靈活運(yùn)用不同級別的鎖,從而在保證并發(fā)安全性的同時(shí),最大程度地提升程序的運(yùn)行效率。
本文將深入探討"Synchronized"的鎖升級過程,詳細(xì)介紹從無鎖狀態(tài)到偏向鎖、輕量級鎖,直至重量級鎖的不同階段及其背后的原理。
Synchronized鎖的基礎(chǔ)概念
在Java中,synchronized關(guān)鍵字是實(shí)現(xiàn)線程同步的關(guān)鍵機(jī)制之一,它用于確保多個(gè)線程在訪問共享資源時(shí)的正確性和一致性。synchronized鎖的基本思想是,當(dāng)一個(gè)線程進(jìn)入某個(gè)synchronized代碼塊或方法時(shí),它必須首先獲取到該對象或類的鎖,然后才能執(zhí)行相應(yīng)的操作。如果其他線程試圖進(jìn)入相同的synchronized區(qū)域,它們將被阻塞,直到鎖被釋放。
對象頭與Mark Word簡介
Java對象在內(nèi)存中不僅包含類實(shí)例的字段,還包含一些元數(shù)據(jù),這些元數(shù)據(jù)存儲在對象頭中。對象頭是Java對象的重要組成部分,它包含了關(guān)于對象的重要信息,如哈希碼、GC年齡以及鎖狀態(tài)等。其中,Mark Word是對象頭中的一個(gè)關(guān)鍵字段,它記錄了關(guān)于對象鎖狀態(tài)的信息。通過修改Mark Word的內(nèi)容,JVM能夠?qū)崿F(xiàn)對對象鎖的獲取和釋放。
Synchronized鎖定的基本原理與運(yùn)作機(jī)制概述
synchronized鎖定的基本原理是通過對對象或類的監(jiān)視器(Monitor)進(jìn)行加鎖和解鎖操作來實(shí)現(xiàn)線程同步。當(dāng)一個(gè)線程嘗試進(jìn)入synchronized代碼塊或方法時(shí),它會首先嘗試獲取對象或類的鎖。如果鎖已經(jīng)被其他線程持有,則該線程將被阻塞,直到鎖被釋放。synchronized鎖的運(yùn)作機(jī)制包括偏向鎖、輕量級鎖和重量級鎖三種狀態(tài)。偏向鎖適用于單線程訪問的情況,輕量級鎖適用于多線程競爭不激烈的情況,而重量級鎖則用于處理高競爭場景。通過這三種狀態(tài)的轉(zhuǎn)換,synchronized鎖能夠根據(jù)不同的并發(fā)場景動態(tài)調(diào)整鎖策略,以實(shí)現(xiàn)高效的線程同步。
關(guān)于synchronized的實(shí)現(xiàn)方式,原理介紹,請參考:美團(tuán)一面:說說_synchronized_的實(shí)現(xiàn)原理?問麻了。。。。
鎖升級的概念
鎖升級是指Java虛擬機(jī)(JVM)在并發(fā)環(huán)境下對synchronized關(guān)鍵字所使用的鎖機(jī)制進(jìn)行動態(tài)調(diào)整的過程,從最初的無鎖狀態(tài)逐漸過渡到偏向鎖、輕量級鎖,直至最終的重量級鎖。這一過程旨在根據(jù)實(shí)際的并發(fā)狀況選擇最適合的鎖類型,以實(shí)現(xiàn)對共享資源的最佳保護(hù)和最有效的并發(fā)控制。
鎖升級的主要目的是為了提升并發(fā)性能,減少不必要的線程上下文切換和內(nèi)存消耗。線程上下文切換是一個(gè)相對昂貴的操作,因?yàn)樗婕暗奖4娈?dāng)前線程的狀態(tài)、恢復(fù)另一個(gè)線程的狀態(tài)等一系列操作。通過優(yōu)化鎖策略,JVM可以減少這種切換的頻率,從而提高系統(tǒng)的整體性能。
另外,鎖升級也有助于減少內(nèi)存消耗。相較于重量級鎖需要創(chuàng)建額外的Monitor對象并在操作系統(tǒng)層面進(jìn)行線程調(diào)度,偏向鎖和輕量級鎖在一定程度上降低了內(nèi)存消耗,特別是對于大量短生命周期的鎖請求場景。
Synchronized鎖的四種狀態(tài)詳解
當(dāng)我們使用synchronized時(shí),Java虛擬機(jī)(JVM)會為每個(gè)被同步的對象維護(hù)一個(gè)鎖(或稱為監(jiān)視器鎖)。這個(gè)鎖有四種狀態(tài):從級別由低到高依次是:無鎖、偏向鎖,輕量級鎖,重量級鎖,用于控制多線程對共享資源的訪問。
四種鎖狀態(tài)的轉(zhuǎn)換
無鎖
無鎖狀態(tài)是對象初始化后的默認(rèn)鎖狀態(tài),表示對象當(dāng)前未被任何線程鎖定。在這種狀態(tài)下,對象頭的鎖標(biāo)志位通常為空或特定的無鎖標(biāo)識,表明對象不受任何同步控制,任何線程都能夠無障礙地訪問該對象。
無鎖的標(biāo)志位為01,即如果是否偏向鎖標(biāo)識為0時(shí)是無鎖狀態(tài),為1時(shí)是偏向鎖。在這個(gè)狀態(tài)下,沒有線程擁有鎖,并且存儲了對象的hashcode、對象的分代年齡以及是否為偏向鎖的標(biāo)志(0表示不是偏向鎖)。
當(dāng)一個(gè)線程首次嘗試獲取鎖時(shí),JVM會檢查這個(gè)鎖是否處于無鎖狀態(tài)。如果是,JVM會嘗試將鎖偏向給這個(gè)線程,也就是將鎖標(biāo)記為偏向這個(gè)線程,并且將這個(gè)線程的ID記錄在鎖的標(biāo)記中。這樣,當(dāng)這個(gè)線程再次嘗試獲取鎖時(shí),就可以避免一些昂貴的操作,因?yàn)镴VM可以直接檢查鎖是否仍然偏向這個(gè)線程。
偏向鎖
當(dāng)一個(gè)線程首次成功獲取一個(gè)鎖時(shí),鎖就進(jìn)入了偏向鎖狀態(tài)。在偏向鎖狀態(tài)下,只有持有偏向鎖的線程才能再次獲取這個(gè)鎖,而不會引起競爭。如果其他線程嘗試獲取這個(gè)鎖,偏向鎖就會升級為輕量級鎖。
偏向鎖的標(biāo)志位為01,即是否偏向鎖表標(biāo)識位為1。與無鎖狀態(tài)的標(biāo)志位相同,但存儲的內(nèi)容有所不同。偏向鎖狀態(tài)下,會存儲偏向的線程ID、偏向時(shí)間戳、對象分代年齡以及是否偏向鎖的標(biāo)志(1)。
偏向鎖是一種針對線程獨(dú)占鎖優(yōu)化的機(jī)制,它適用于單一線程長時(shí)間、連續(xù)地訪問同一段同步代碼的情況。當(dāng)某個(gè)線程首次獲得同步代碼塊的鎖后,Java虛擬機(jī)會在對象頭的Mark Word中記錄該線程的ID,形成偏向鎖。在此之后,該線程再次進(jìn)入同步代碼塊時(shí),無需執(zhí)行CAS操作等復(fù)雜的同步動作,僅需確認(rèn)Mark Word中的偏向線程ID是否為自己,便可迅速獲得鎖,從而極大地減少了獲取鎖的開銷,提升了并發(fā)性能。
在偏向鎖生效期間,除非有其他線程嘗試獲取該鎖,否則持有偏向鎖的線程不會主動釋放鎖。當(dāng)出現(xiàn)鎖競爭時(shí),原有的偏向鎖持有者會經(jīng)歷撤銷過程。此過程發(fā)生在全局安全點(diǎn),即在所有線程均停止執(zhí)行字節(jié)碼的時(shí)刻,JVM會暫停當(dāng)前持有偏向鎖的線程,檢查鎖對象的狀態(tài)。如果發(fā)現(xiàn)持有偏向鎖的線程不再活動或者鎖確實(shí)處于被爭奪狀態(tài),則會撤銷偏向鎖,即將對象頭恢復(fù)為無鎖狀態(tài)(標(biāo)志位為01)或直接升級為輕量級鎖(標(biāo)志位調(diào)整為對應(yīng)輕量級鎖的狀態(tài))。
偏向鎖主要是為了解決在一個(gè)線程連續(xù)多次獲取同一鎖的情況,降低不必要的同步操作開銷。當(dāng)首次獲取鎖的線程再次進(jìn)入同步代碼塊時(shí),會檢查對象頭中存儲的線程ID是否與當(dāng)前線程一致。如果一致,則直接獲得鎖;如果不一致,則需要撤銷偏向鎖,重新進(jìn)行鎖競爭,可能升級為輕量級鎖。
優(yōu)點(diǎn): 對于沒有或很少發(fā)生鎖競爭的場景,偏向鎖可以顯著減少鎖的獲取和釋放所帶來的性能損耗。
缺點(diǎn):
- ? 額外存儲空間:偏向鎖會在對象頭中存儲一個(gè)偏向線程ID等相關(guān)信息,這部分額外的空間開銷雖然較小,但在大規(guī)模并發(fā)場景下,累積起來也可能成為可觀的成本。
- ? 鎖升級開銷:當(dāng)一個(gè)偏向鎖的對象被其他線程訪問時(shí),需要進(jìn)行撤銷(revoke)操作,將偏向鎖升級為輕量級鎖,甚至在更高競爭情況下升級為重量級鎖。這個(gè)升級過程涉及到CAS操作以及可能的線程掛起和喚醒,會帶來一定的性能開銷。
- ? 適用場景有限:偏向鎖最適合于絕大部分時(shí)間只有一個(gè)線程訪問對象的場景,這樣的情況下,偏向鎖的開銷可以降到最低,有利于提高程序性能。但如果并發(fā)程度較高,或者線程切換頻繁,偏向鎖就可能不如輕量級鎖或重量級鎖高效。
輕量級鎖
當(dāng)一個(gè)線程嘗試獲取一個(gè)已經(jīng)被其他線程持有的偏向鎖時(shí),偏向鎖會升級為輕量級鎖。輕量級鎖是一種用于處理線程之間輕量級競爭的機(jī)制。當(dāng)一個(gè)線程嘗試獲取輕量級鎖時(shí),它會先自旋一段時(shí)間,嘗試等待鎖被釋放。如果在這段時(shí)間內(nèi)鎖被釋放了,那么這個(gè)線程就可以成功獲取鎖。如果自旋結(jié)束后鎖仍然被持有,那么這個(gè)線程就會嘗試將鎖升級為重量級鎖。
輕量級鎖的標(biāo)識位為:00。當(dāng)鎖從偏向鎖升級為輕量級鎖時(shí),標(biāo)志位會變?yōu)?0。在輕量級鎖狀態(tài)下,多個(gè)線程可能會嘗試獲取鎖,通過自旋來等待鎖被釋放。
輕量級鎖利用CAS操作嘗試將對象頭的Mark Word替換為指向線程棧中鎖記錄的指針,如果CAS操作成功,則表示線程成功獲取鎖。獲取鎖失敗的線程會進(jìn)入自旋狀態(tài),不斷循環(huán)嘗試獲取鎖,直到獲取成功或升級為重量級鎖。在自旋期間,線程不會立即進(jìn)入阻塞狀態(tài),而是不斷循環(huán)檢查鎖是否可用。這種機(jī)制可以減少線程上下文切換的開銷,但如果自旋次數(shù)過多或者競爭加劇,自旋就會失去意義,JVM會選擇升級為重量級鎖。
優(yōu)點(diǎn):
? 低開銷:輕量級鎖通過CAS操作嘗試獲取鎖,避免了重量級鎖中涉及的線程掛起和恢復(fù)等高昂開銷。
? 快速響應(yīng):在無鎖競爭或者鎖競爭不激烈的情況下,輕量級鎖使得線程可以迅速獲取鎖并執(zhí)行同步代碼塊。
缺點(diǎn):
? 自旋消耗:當(dāng)鎖競爭激烈時(shí),線程可能會長時(shí)間自旋等待鎖,這會消耗CPU資源,導(dǎo)致性能下降。
? 升級開銷:如果自旋等待超過一定閾值或者鎖競爭加劇,輕量級鎖會升級為重量級鎖,這個(gè)升級過程本身也有一定的開銷。
重量級鎖
當(dāng)輕量級鎖的自旋嘗試達(dá)到一定閾值,或者檢測到多個(gè)線程競爭激烈時(shí),JVM會將輕量級鎖升級為重量級鎖。升級過程中,會取消當(dāng)前線程的自旋操作,并在對象頭中設(shè)置重量級鎖標(biāo)志。
重量級鎖的標(biāo)識位為:10。當(dāng)鎖從輕量級鎖升級為重量級鎖時(shí),標(biāo)志位會變?yōu)?0。在重量級鎖狀態(tài)下,線程在獲取鎖時(shí)會阻塞,直到持有鎖的線程釋放鎖。
在重量級鎖狀態(tài)下,線程在獲取鎖失敗時(shí)會被操作系統(tǒng)掛起,放入到該對象關(guān)聯(lián)的監(jiān)視器(Monitor)的等待隊(duì)列中,由操作系統(tǒng)進(jìn)行線程調(diào)度,當(dāng)鎖被釋放時(shí),操作系統(tǒng)會選擇合適的線程將其喚醒并授予鎖。
盡管重量級鎖的開銷較大,涉及到線程上下文切換和內(nèi)核態(tài)用戶態(tài)的切換等,但它在高競爭場景下能提供穩(wěn)定的互斥性和公平性,確保數(shù)據(jù)的一致性和線程的安全執(zhí)行。因此,即使性能損耗較高,也是在特定情況下必要的權(quán)衡措施。
優(yōu)點(diǎn):
? 強(qiáng)一致性:重量級鎖提供了最強(qiáng)的線程安全性,確保在多線程環(huán)境下數(shù)據(jù)的完整性和一致性。
? 簡單易用:synchronized關(guān)鍵字的使用簡潔明了,不易出錯。
缺點(diǎn):
? 性能開銷大:獲取和釋放重量級鎖時(shí)需要操作系統(tǒng)介入,可能涉及線程的掛起和喚醒,造成上下文切換,這對于頻繁鎖競爭的場景來說性能代價(jià)較高。
? 延遲較高:線程獲取不到鎖時(shí)會被阻塞,導(dǎo)致等待時(shí)間增加,進(jìn)而影響系統(tǒng)響應(yīng)速度。
以上四種鎖狀態(tài)優(yōu)缺點(diǎn)對比總結(jié)如下:
類型 | 優(yōu)點(diǎn) | 缺點(diǎn) | 使用場景 |
偏向鎖 | 快速:無須線程上下文切換,適合單一線程多次重復(fù)獲取同一線程鎖的場景 低開銷:只需要檢查對象頭標(biāo)記 | 不適合多線程競爭的場景 競爭時(shí)需要撤銷偏向鎖,有一定開銷 | 大多數(shù)時(shí)候只有一線程訪問同步代碼塊,很少出現(xiàn)鎖競爭的情況 |
輕量級鎖 | 較快:通過CAS操作和自旋避免了線程的阻塞與喚醒,減少了線程上下文切換 適用于鎖競爭不激烈的場景 | 自旋可能導(dǎo)致CPU空耗,在高競爭下,大量的線程自旋會增加系統(tǒng)負(fù)擔(dān)。 無法保證絕對的公平性 | 短時(shí)間的同步代碼塊,且鎖競爭不激烈,期望快速重入和釋放 |
重量級鎖 | 穩(wěn)定可靠:嚴(yán)格保證互斥性和公平性 能夠有效應(yīng)對高度競爭的鎖場景 | 開銷大:涉及到線程上下文切換,性能較低 阻塞線程可能導(dǎo)致響應(yīng)時(shí)間變長 | 高并發(fā)、高競爭的場景,需要保證數(shù)據(jù)一致性,且線程等待鎖的時(shí)間較長或不可預(yù)知 |
關(guān)于Java中鎖的分類,以及各種所得介紹,請參考:阿里二面:Java中_鎖的分類_有哪些?你能說全嗎?
關(guān)于Java中如何定位以及避免死鎖,請參考:阿里二面:如何定位&避免_死鎖_?連著兩個(gè)面試問到了!
鎖升級的具體步驟與流程
1.無鎖到偏向鎖的升級流程:
? 當(dāng)線程首次嘗試獲取對象鎖時(shí),JVM首先檢查對象是否處于無鎖狀態(tài)。
? 若處于無鎖狀態(tài),JVM則立即將其標(biāo)記為偏向鎖,并記錄下當(dāng)前線程的ID。
? 這一過程通過CAS操作實(shí)現(xiàn),確保線程安全地更新對象頭的Mark Word為偏向鎖狀態(tài),并保存偏向線程的ID。
? 一旦設(shè)置成功,線程便可無阻礙地進(jìn)入同步代碼塊,后續(xù)再次獲取該鎖時(shí)僅需驗(yàn)證是否仍偏向當(dāng)前線程,無需額外同步操作
而對于偏向鎖的釋放機(jī)制:
? 當(dāng)持有偏向鎖的線程正常退出同步代碼塊時(shí),JVM僅簡單地更新對象頭的訪問計(jì)數(shù)等相關(guān)信息。
? 由于偏向鎖的設(shè)計(jì)初衷是優(yōu)化同一線程對鎖的反復(fù)獲取,因此它并不會立即釋放偏向關(guān)系,而是假設(shè)下一次仍由同一線程獲取鎖。
2. 偏向鎖到輕量級鎖的升級流程:
? 當(dāng)?shù)诙€(gè)線程嘗試獲取已被偏向的鎖時(shí),它會首先校驗(yàn)對象頭是否指向當(dāng)前線程的ID。
? 若校驗(yàn)失敗,表明鎖已偏向其他線程,此時(shí)需要撤銷偏向鎖。
? 撤銷后,對象會回到無鎖狀態(tài)或過渡至輕量級鎖狀態(tài)。
? 接著,新線程會嘗試在其棧幀中創(chuàng)建鎖記錄,并使用CAS操作將對象頭的Mark Word替換為指向該鎖記錄的指針。
? 若CAS操作成功,線程即獲得輕量級鎖;若失敗,則進(jìn)入自旋狀態(tài),循環(huán)嘗試獲取鎖。
對于輕量級鎖的釋放機(jī)制:
? 持有輕量級鎖的線程在退出同步代碼塊時(shí),會嘗試通過CAS操作將對象頭恢復(fù)為原始狀態(tài),即撤銷鎖記錄指針的替換。
? 若CAS操作成功,則輕量級鎖被順利釋放;否則,可能需要進(jìn)一步的鎖升級或處理。
3. 輕量級鎖到重量級鎖的升級流程:
? 當(dāng)輕量級鎖的持有線程退出同步代碼塊并釋放鎖時(shí),它會嘗試將對象頭恢復(fù)到無鎖或偏向鎖狀態(tài)。
? 若存在多個(gè)線程競爭鎖資源,輕量級鎖的釋放可能導(dǎo)致自旋線程長時(shí)間無法獲取鎖。
? JVM會綜合考量自旋次數(shù)、競爭激烈程度以及系統(tǒng)負(fù)載等因素,決策是否將輕量級鎖升級為重量級鎖。
? 一旦升級為重量級鎖,原持有線程必須完成鎖的釋放。新來的線程將被阻塞,并被加入對象的監(jiān)視器(Monitor)等待隊(duì)列,由操作系統(tǒng)負(fù)責(zé)線程的調(diào)度管理。
對于釋放重量級鎖:
? 持有重量級鎖的線程在退出同步代碼塊時(shí),會通過調(diào)用Monitor的釋放操作來喚醒等待隊(duì)列中的下一個(gè)線程。
? 被喚醒的線程將獲得鎖并繼續(xù)執(zhí)行同步代碼,確保資源的順序訪問和線程安全
鎖的升級流程
鎖降級與鎖消除
鎖降級
鎖降級通常出現(xiàn)在使用讀寫鎖(如Java中的ReentrantReadWriteLock)的場景中。在多線程環(huán)境下,一個(gè)線程首先獲取到了寫鎖,那么在它持有寫鎖期間,任何其他線程都無法獲取讀鎖或?qū)戞i,確保了對該資源的獨(dú)占訪問權(quán)以進(jìn)行修改。這個(gè)在持有寫鎖的同時(shí),線程會嘗試獲取讀鎖。由于該線程已經(jīng)持有寫鎖,所以它可以成功獲取讀鎖,而不會造成死鎖或其他同步問題。然后線程釋會放寫鎖,但仍持有讀鎖。此時(shí),其他線程可以獲取讀鎖進(jìn)行讀取操作,但無法獲取寫鎖進(jìn)行寫入操作。
鎖降級的意義在于,線程在完成寫操作后,如果接下來的任務(wù)主要是讀取而不是繼續(xù)寫入,那么通過降級能夠允許其他讀線程同時(shí)訪問資源,提高了系統(tǒng)的并發(fā)性能,同時(shí)保證了數(shù)據(jù)一致性,因?yàn)樗凶x線程看到的都是最近一次寫操作完成后的一致性視圖。鎖降級是針對讀寫鎖的一種高級使用方式,用于提升多讀少寫的并發(fā)場景性能。
鎖消除
鎖消除(Lock Elimination)是一種由編譯器或虛擬機(jī)在運(yùn)行時(shí)進(jìn)行的優(yōu)化技術(shù),其目的是去除那些不必要的鎖操作。當(dāng)編譯器或JVM的即時(shí)編譯器(JIT Compiler)在分析代碼時(shí)發(fā)現(xiàn)某個(gè)鎖保護(hù)的變量并沒有發(fā)生實(shí)際的共享數(shù)據(jù)競爭,也就是說,該變量的生命周期僅限于方法內(nèi)部,不會逃逸出該方法,那么這個(gè)鎖就可以安全地被消除掉。
例如,如果一段同步代碼塊中的變量只在棧上分配并且沒有其他線程可以直接訪問,那么即使對該變量進(jìn)行了同步也不會帶來任何好處,反而增加了上下文切換和鎖獲取釋放的開銷。在這種情況下,JVM可以通過逃逸分析等手段確定該變量不存在共享狀態(tài),進(jìn)而消除對它的同步操作。
鎖消除則是編譯器和JVM層面的一種優(yōu)化技術(shù),用于消除不必要的同步,減少鎖帶來的性能損耗。
總結(jié)
Synchronized鎖升級機(jī)制是Java虛擬機(jī)為優(yōu)化多線程環(huán)境下同步操作性能而設(shè)計(jì)的一種動態(tài)調(diào)整策略。通過偏向鎖、輕量級鎖和重量級鎖之間的智能轉(zhuǎn)換,JVM可以根據(jù)實(shí)際的并發(fā)狀況在低競爭和高競爭場景下分別采取不同的鎖策略,從而有效減少線程上下文切換、內(nèi)存占用以及CPU空轉(zhuǎn)等問題,提升系統(tǒng)的整體并發(fā)性能。
偏向鎖適用于單一線程反復(fù)訪問同一鎖的情況,輕量級鎖則在輕度競爭場景下通過CAS和自旋優(yōu)化鎖的獲取和釋放,而重量級鎖雖然開銷較大,但在高強(qiáng)度競爭下提供了嚴(yán)格的互斥性和線程調(diào)度的公平性。