【Java】Synchronized-你知道Java是如何上鎖的嗎?
Synchronized鎖獲取與升級(jí)流程——從偏向鎖到重量級(jí)鎖
synchronized 關(guān)鍵字是 Java 并發(fā)編程的元老,很多人對(duì)它的印象還停留在“重量級(jí)”、“性能差”。但從 JDK 1.6 開始,synchronized 引入了鎖升級(jí)機(jī)制,使其變得非常智能。
這套機(jī)制的核心思想是“按需分配”,在無競爭或低競爭時(shí)提供極高的性能,只在競爭激烈時(shí)才退化為傳統(tǒng)的重量級(jí)鎖。它的升級(jí)路徑如下,并且此過程不可逆(只能升級(jí),不能降級(jí)):
無鎖 → 偏向鎖 → 輕量級(jí)鎖 → 重量級(jí)鎖
1. 無鎖狀態(tài) (No Lock)
一個(gè)對(duì)象在剛被創(chuàng)建時(shí),其對(duì)象頭的 Mark Word 中沒有任何鎖信息,此時(shí)它就處于無鎖狀態(tài)。
2. 偏向鎖 (Biased Lock)
升級(jí)時(shí)機(jī):當(dāng)?shù)谝粋€(gè)線程訪問同步代碼塊時(shí),鎖會(huì)升級(jí)為偏向鎖。
核心機(jī)制:
通過 CAS (Compare-And-Swap) 操作,將當(dāng)前線程的 ID 記錄在對(duì)象頭的 Mark Word 中。
此后,該線程再次進(jìn)入或退出同步塊時(shí),不再需要進(jìn)行任何 CAS 操作,只需簡單檢查 Mark Word 中的線程 ID 是否是自己。
目標(biāo)場景:適用于只有一個(gè)線程會(huì)訪問同步代碼塊的場景。這大大減少了加鎖的開銷,性能幾乎等同于無鎖。
3. 輕量級(jí)鎖 (Lightweight Lock)
升級(jí)時(shí)機(jī):當(dāng)?shù)诙€(gè)線程嘗試獲取這個(gè)偏向鎖時(shí),偏向鎖模式宣告結(jié)束,進(jìn)入偏向鎖撤銷(Bias Revocation)流程,并升級(jí)為輕量級(jí)鎖。
核心機(jī)制:
線程在自己的棧幀中創(chuàng)建一塊名為 Lock Record 的空間,用于拷貝對(duì)象頭的 Mark Word(Displaced Mark Word)。
通過 CAS 操作,嘗試將對(duì)象頭的 Mark Word 更新為指向這個(gè) Lock Record 的指針。
如果 CAS 成功,則獲取鎖成功。如果失敗,線程會(huì)進(jìn)行自適應(yīng)自旋(Adaptive Spinning),即在原地循環(huán)一小段時(shí)間,而不是立即掛起,期待鎖能很快被釋放。
目標(biāo)場景:適用于線程交替執(zhí)行同步代碼塊,且每次持有鎖的時(shí)間很短的場景。自旋避免了線程掛起和喚醒帶來的系統(tǒng)開銷。
4. 重量級(jí)鎖 (Heavyweight Lock)
升級(jí)時(shí)機(jī):當(dāng)輕量級(jí)鎖的自旋失敗(例如,超過自旋次數(shù)或有其他線程也在自旋),鎖就會(huì)膨脹(Inflate)為重量級(jí)鎖。
核心機(jī)制:
鎖依賴于操作系統(tǒng)底層的 Mutex Lock(互斥量) 來實(shí)現(xiàn)。
未能獲取鎖的線程不再自旋,而是會(huì)被掛起(BLOCKED),并進(jìn)入一個(gè)等待隊(duì)列。
等待持有鎖的線程釋放鎖后,再由操作系統(tǒng)喚醒其中一個(gè)等待的線程。
目標(biāo)場景:適用于高并發(fā)、高競爭的場景。雖然上下文切換(用戶態(tài) ? 內(nèi)核態(tài))開銷巨大,但它能保證在激烈競爭下,所有線程都能有序地獲取鎖。
總結(jié)
| 鎖狀態(tài) | 核心機(jī)制 | 適用場景 | 開銷 |
|---|---|---|---|
| 無鎖 | - | 無線程競爭 | 無 |
| 偏向鎖 | CAS 記錄線程 ID,后續(xù)僅檢查 | 單線程訪問 | 極低 |
| 輕量級(jí)鎖 | CAS + 自適應(yīng)自旋 | 線程交替執(zhí)行,競爭不激烈 | CPU 自旋開銷 |
| 重量級(jí)鎖 | 操作系統(tǒng) Mutex,線程掛起/喚醒 | 高并發(fā),競爭激烈 | 重量級(jí),涉及內(nèi)核態(tài)切換 |

synchronized 通過這套精巧的鎖升級(jí)機(jī)制,在性能和線程安全之間取得了完美的平衡。它不再是那個(gè)“笨重”的鎖,而是一個(gè)能夠根據(jù)競爭情況自動(dòng)調(diào)整策略的智能鎖。
揭秘 Synchronized 的可重入性:它是如何實(shí)現(xiàn)的?
synchronized 的可重入性(Reentrancy)是其核心特性之一,它保證了同一個(gè)線程可以多次獲取自己已經(jīng)持有的鎖,從而避免了自己把自己鎖死(死鎖)的情況。
1. 什么是可重入性?為什么它很重要?
想象以下場景:一個(gè)類中有兩個(gè) synchronized 方法,其中一個(gè)方法調(diào)用了另一個(gè)。
public class ReentrantExample {
public synchronized void methodA() {
System.out.println("Executing methodA...");
methodB(); // 調(diào)用另一個(gè)同步方法
}
public synchronized void methodB() {
System.out.println("Executing methodB...");
}
}
如果沒有可重入性,當(dāng)一個(gè)線程執(zhí)行 methodA() 時(shí),它獲取了對(duì)象的鎖。接著,它嘗試調(diào)用 methodB(),此時(shí)它需要再次獲取同一個(gè)對(duì)象的鎖。由于鎖已經(jīng)被自己持有,它會(huì)陷入無限等待,造成死鎖。
可重入性解決了這個(gè)問題:它允許已經(jīng)持有鎖的線程,無需等待,直接再次獲取該鎖。
2. Synchronized 如何在不同鎖狀態(tài)下保證可重入?
synchronized 的可重入性設(shè)計(jì)貫穿了從偏向鎖到重量級(jí)鎖的整個(gè)升級(jí)過程,確保在任何狀態(tài)下行為一致。
a) 偏向鎖階段 (Biased Lock)
這是實(shí)現(xiàn)最簡單、開銷最小的階段。
機(jī)制:線程進(jìn)入同步塊時(shí),只需檢查對(duì)象頭 Mark Word 中記錄的線程 ID 是否是自己。
重入:如果是,直接進(jìn)入,無需任何額外操作。這就像進(jìn)自己家門,刷臉就行。
b) 輕量級(jí)鎖階段 (Lightweight Lock)
當(dāng)鎖升級(jí)為輕量級(jí)鎖后,機(jī)制變得復(fù)雜一些。
機(jī)制:線程會(huì)在自己的棧幀中創(chuàng)建 Lock Record 來持有鎖。
重入:當(dāng)線程嘗試重入時(shí),它會(huì)發(fā)現(xiàn)對(duì)象頭已經(jīng)指向一個(gè)位于自己棧幀的 Lock Record。此時(shí),JVM 會(huì)在當(dāng)前棧幀中再創(chuàng)建一個(gè) Lock Record,但將其內(nèi)部的 displaced_header 字段設(shè)為 null。這個(gè) null 標(biāo)記就代表了一次重入。退出同步塊時(shí),每遇到一個(gè) null 的 Lock Record,就代表完成一次重入解鎖。
c) 重量級(jí)鎖階段 (Heavyweight Lock)
這是最經(jīng)典、最廣為人知的實(shí)現(xiàn)方式。
機(jī)制:鎖由底層的 ObjectMonitor 對(duì)象管理。
重入:ObjectMonitor 內(nèi)部有一個(gè)名為 _recursions 或 _count 的計(jì)數(shù)器。當(dāng)一個(gè)線程獲取鎖后,計(jì)數(shù)器變?yōu)?1。該線程每重入一次,計(jì)數(shù)器就 +1。相應(yīng)地,每退出一個(gè)同步塊,計(jì)數(shù)器就 -1。直到計(jì)數(shù)器歸零,鎖才被真正釋放。
3. 總結(jié)
synchronized 的可重入性是其內(nèi)在基因,通過在不同鎖狀態(tài)下采用不同的策略(檢查線程ID、設(shè)置null標(biāo)記、維護(hù)計(jì)數(shù)器),無縫地保證了同一線程可以安全、高效地多次進(jìn)入同步代碼塊。
這種精巧的設(shè)計(jì),使得開發(fā)者可以放心地在同步方法中調(diào)用其他同步方法,而不必?fù)?dān)心死鎖問題,極大地增強(qiáng)了 synchronized 關(guān)鍵字的健壯性和易用性。
為什么說 Synchronized 是一個(gè)“不公平”的鎖?
在面試或技術(shù)討論中,我們常聽到一個(gè)結(jié)論:synchronized 是一個(gè)非公平鎖(Non-fair Lock)。
這個(gè)“不公平”聽起來像個(gè)缺點(diǎn),但實(shí)際上,它是 synchronized 為了性能而做出的一個(gè)精明權(quán)衡。本文將帶你徹底搞懂其中的緣由。
1. 首先,什么是公平與非公平?
讓我們用一個(gè)生活中的例子來理解:
公平鎖 (Fair Lock):就像在銀行排隊(duì)辦業(yè)務(wù),講究“先來后到”。排在隊(duì)首的客戶(線程)一定比隊(duì)尾的客戶先得到服務(wù)。它保證了所有等待的線程最終都能獲得鎖,不會(huì)“餓死”。
非公平鎖 (Non-fair Lock):不遵循嚴(yán)格的排隊(duì)規(guī)則。當(dāng)鎖被釋放時(shí),系統(tǒng)允許一個(gè)新來的、尚未排隊(duì)的線程去“插隊(duì)”,直接嘗試獲取鎖。如果它成功了,就跳過了所有正在排隊(duì)的線程。
synchronized 就屬于后者。
2. Synchronized 的“不公平”體現(xiàn)在哪里?
synchronized 的非公平性貫穿于其鎖機(jī)制中,尤其在從輕量級(jí)鎖升級(jí)到重量級(jí)鎖的過程中表現(xiàn)得淋漓盡致。
a) 輕量級(jí)鎖階段
當(dāng)一個(gè)線程釋放輕量級(jí)鎖時(shí),其他正在自旋等待的線程會(huì)通過 CAS 競爭鎖。這個(gè)競爭本身就是“無序”的,誰的 CAS 操作先成功,誰就獲得鎖,沒有排隊(duì)的概念。
b) 重量級(jí)鎖階段
這是非公平性最核心的體現(xiàn)。當(dāng)鎖膨脹為重量級(jí)鎖后,所有獲取不到鎖的線程都會(huì)被掛起,放入一個(gè)等待隊(duì)列(_EntryList)中。
當(dāng)持有鎖的線程釋放鎖時(shí),JVM 面臨一個(gè)選擇:
- 公平的做法:從等待隊(duì)列的頭部喚醒一個(gè)線程,讓它獲取鎖。
- 非公平的做法:允許一個(gè)剛剛進(jìn)入同步塊、尚未被掛起的新線程,直接嘗試獲取鎖。
HotSpot 虛擬機(jī)的synchronized** 選擇了后者**。 它會(huì)先讓新來的線程嘗試“搶”一下鎖,如果搶不到,這個(gè)新線程才會(huì)被掛起并加入等待隊(duì)列的隊(duì)尾。
3. 為什么要設(shè)計(jì)成“不公平”?—— 性能!
不公平的設(shè)計(jì)看似“不道德”,但它背后是極致的性能追求,核心目標(biāo)是提高系統(tǒng)的總吞吐量 (Throughput)。
這主要是為了避免不必要的上下文切換 (Context Switch)。
- 線程喚醒的成本:喚醒一個(gè)被掛起的線程成本非常高。它需要從內(nèi)核態(tài)切換回用戶態(tài),恢復(fù)線程的運(yùn)行環(huán)境,這比一個(gè)正在運(yùn)行的線程執(zhí)行幾百條指令還要慢。
- 非公平的優(yōu)勢(shì):如果一個(gè)新來的、正在 CPU 上運(yùn)行的線程能夠立即獲取鎖并完成任務(wù),就避免了“喚醒舊線程”和“掛起新線程”這兩次昂貴的上下文切換。雖然對(duì)排隊(duì)的線程不公平,但從整個(gè)系統(tǒng)的角度看,減少了兩次線程狀態(tài)轉(zhuǎn)換,總的執(zhí)行效率更高。
簡單來說,系統(tǒng)認(rèn)為,讓一個(gè)“熱乎”的(正在運(yùn)行的)線程直接干活,比費(fèi)勁去叫醒一個(gè)“睡著”的(被掛起)線程,成本要低得多。
4. 非公平的代價(jià):線程餓死 (Starvation)
非公平鎖的潛在風(fēng)險(xiǎn)是線程餓死。理論上,如果運(yùn)氣極差,一個(gè)排隊(duì)的線程可能會(huì)一直被新來的線程“插隊(duì)”,導(dǎo)致它永遠(yuǎn)也獲取不到鎖。
不過,在實(shí)際應(yīng)用中,這種情況發(fā)生的概率極低。synchronized 的設(shè)計(jì)是在絕大多數(shù)場景下,用極小的“餓死”風(fēng)險(xiǎn)換取顯著的性能提升。
5. 如果我需要公平鎖怎么辦?
如果你有必須保證公平性的業(yè)務(wù)場景(例如,資源需要嚴(yán)格按申請(qǐng)順序分配),可以使用 java.util.concurrent.locks.ReentrantLock。
它提供了一個(gè)構(gòu)造函數(shù)來讓你明確選擇:
// 默認(rèn)構(gòu)造函數(shù),創(chuàng)建一個(gè)非公平鎖 (性能更高)
Lock nonFairLock = new ReentrantLock();
// 傳入 true,創(chuàng)建一個(gè)公平鎖 (保證順序,犧牲部分性能)
Lock fairLock = new ReentrantLock(true);
總結(jié)
synchronized 被設(shè)計(jì)為非公平鎖,并非一個(gè)缺陷,而是一種為性能優(yōu)化的策略。它通過允許新線程“插隊(duì)”,最大限度地減少了昂貴的線程上下文切換,從而提高了系統(tǒng)的整體吞吐量。
這是一個(gè)經(jīng)典的性能與公平性之間的權(quán)衡 (Trade-off)。對(duì)于絕大多數(shù)應(yīng)用場景,synchronized 的這種“不公平”所帶來的性能優(yōu)勢(shì),遠(yuǎn)比其微乎其微的“餓死”風(fēng)險(xiǎn)更有價(jià)值。

浙公網(wǎng)安備 33010602011771號(hào)