Java synchronized的鎖升級過程詳解
介紹
在 JDK 1.6之前,synchronized 是一個重量級、效率比較低下的鎖,但是在JDK 1.6后,JVM 為了提高鎖的獲取與釋放效,,對 synchronized 進行了優(yōu)化,引入了偏向鎖和輕量級鎖,至此,鎖的狀態(tài)有四種,級別由低到高依次為:無鎖、偏向鎖、輕量級鎖、重量級鎖。
鎖升級就是無鎖 —> 偏向鎖 —> 輕量級鎖 —> 重量級鎖 的一個過程,注意,鎖只能升級,不能降級。
原理詳解
對象頭
HotSpot 虛擬機中,對象在內(nèi)存中存儲布局可以分為三塊區(qū)域:對象頭(Header)、實例數(shù)據(jù)(Instance Data)和對齊填充(Padding):

對象頭:分為Mark Word 和 對象指針
- Mark Word:存儲對象自身的運行時數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標志、線程持有的鎖、偏向線程ID、偏向時間戳等。
- 對象指針:存儲指向類元數(shù)據(jù)的指針,使得能夠訪問對象屬于的類的信息。
實例數(shù)據(jù):存儲對象的實際有效信息,也就是我們在類中所定義的各種類型的字段內(nèi)容。
對齊填充:可選字段,通常存在于對象的末尾,用于確保對象的大小是8字節(jié)的倍數(shù)(因為許多JVM都使用8字節(jié)的對象對齊)。這是出于性能考慮,使得對象的地址在內(nèi)存中是對齊的。
synchronized 鎖相關(guān)的信息主要是在 Mark Word 區(qū)域,我們先看看 Mark Word。
Mark Word
synchronized 用的鎖存在鎖對象的對象頭的Mark Word中,我們先看 Mark Word 到底長什么樣。

鎖分類
無鎖

無鎖可以理解為單線程輕松愉快地運行,沒有其他的線程來和其競爭。但是無鎖不代表沒有同步,它只是表示鎖對象目前沒有被任何線程顯式鎖定。
偏向鎖
偏向鎖 JDK 1.6 引入的一種鎖優(yōu)化機制。

何謂“偏向”?就是鎖對象會偏向于第一個獲得它的線程。什么意思呢。
當一個線程訪問同步代碼塊并獲取鎖時,該鎖會進入偏向模式,鎖標志的狀態(tài)將被設(shè)置為偏向(01),并且鎖的擁有者被設(shè)置為當前線程(偏向鎖線程 id = 當前線程 id)。當該線程執(zhí)行完同步代碼塊后,線程并不會主動釋放偏向鎖。當線程再次進入同步代碼塊時,會首先判斷此時持有鎖的線程與它是否為同一線程,如果是則正常往下執(zhí)行,由于此前是沒有釋放鎖的,所以這次就不會有任何的獲取鎖操作。
所以,偏向鎖是指當一段同步代碼一直被同一個線程所訪問時,就不存在所謂的多線程競爭了,那么該線程在后續(xù)訪問時便會自動獲得鎖,從而降低獲取鎖帶來的消耗,即提高性能。
偏向鎖的鎖釋放是一個被動過程,線程不會主動釋放偏向鎖,只有當其他線程來競爭偏向鎖時,JVM 才會檢測到鎖的狀態(tài)并觸發(fā)撤銷。但是撤銷需要等待全局安全點(所有線程會暫停),JVM 會在全局安全點時判斷鎖對象是否處于被鎖定狀態(tài),如果沒有被鎖定,且持有鎖的線程不處于活動狀態(tài),則將對象頭設(shè)置為無鎖狀態(tài),并撤銷偏向鎖。
所以,引入偏向鎖的目的是認為當前環(huán)境下是不存在多線程競爭的場景,可以認為是單線程環(huán)境,同一個線程多次持有鎖,減少單線程環(huán)境下獲取鎖帶來的不必要。
流程圖如下:

輕量級鎖
當一個線程持有偏向鎖時,另外一個線程來競爭鎖,這時偏向鎖就會升級為輕量級鎖。

輕量級鎖的競爭方式一種比較輕量級的競爭方式,當某個線程沒有獲取到鎖,它并不是立刻被掛起,而是采取自旋的方式來競爭鎖資源。在競爭較少的情況下,輕量級鎖通過減少線程阻塞和喚醒操作,可以提高性能。
輕量級鎖的目的在于它認為系統(tǒng)當前的競爭環(huán)境不是激烈,如果采取阻塞和喚醒線程的方式,則會過多地消耗系統(tǒng)資源。如果某個線程沒有獲取到輕量級鎖,則采取自旋的方式來判斷鎖資源是否已被釋放。這種方式減少了上線文的切換。
但是長時間的自旋操作是非常消耗資源的,一個線程獲取了輕量級鎖,其他線程就只能在那里“空耗”,它們不釋放 CPU 資源,但也不做任何事,這種現(xiàn)象叫做忙等(busy-waiting)。所以,我們是允許短時間的忙等,用它來換取線程在用戶態(tài)和內(nèi)核態(tài)之間切換的開銷。
觸發(fā)輕量級鎖的條件是兩個:
- 關(guān)閉偏向鎖(
-XX:-UseBiasedLocking) - 多個線程競爭偏向鎖導(dǎo)致偏向鎖升級為輕量級鎖
流程圖如下:

重量級鎖
輕量級鎖自旋是要有限度的,你不能一直在那里空轉(zhuǎn),所以如果鎖競爭環(huán)境比較嚴重,當自旋次數(shù)達到某個閾值(默認 10 次,可自動調(diào)整)后,就是停止自旋,此時鎖膨脹為重量級鎖。當其膨脹為重量級鎖后,其他線程就不再是等待了,而是阻塞等待。重量級鎖依賴對象內(nèi)部的監(jiān)視器(monitor)實現(xiàn),而 monitor 依賴的是操作系統(tǒng)的 MutexLock(互斥鎖)。
由于是重量級鎖,那么等待鎖資源的線程都會被阻塞,雖然阻塞的線程不會消耗 CPU,但是阻塞或者喚醒一個線程都需要通過底層操作系統(tǒng)來實現(xiàn),它會涉及到上下文切換,用戶態(tài)和內(nèi)核態(tài)之間的轉(zhuǎn)換,這本身就是一個非常重量級、高開銷的操作。

鎖升級過程
鎖升級就是無鎖 —> 偏向鎖 —> 輕量級鎖 —> 重量級鎖 的一個過程,注意,鎖只能升級,不能降級。流程圖如下:

- JVM 啟動后,鎖資源對象直到有第一個線程訪問時,它都是無鎖狀態(tài),此時 Mark Word 內(nèi)容如下:

偏向鎖標識為 0,鎖標識為 01。
- 當鎖對象首次被某個線程(假如為線程 A,id 為
1000001)時,鎖就會從無鎖狀態(tài)升級偏向鎖。偏向鎖會在 Mark Word 中的偏向鎖線程 id 存儲當前線程的id(1000001),偏向鎖標識為 1,鎖標識為 01,如下:

如果當前線程再次獲取該鎖對象,只需要比較偏向鎖線程 id 即可。
- 當有其他線程(假如為線程 B,id 為
1000002)來競爭該鎖對象,此時鎖為偏向鎖,這個時候會比較偏向鎖的線程 id 是否為線程 B1000002,我們可以判斷不是,所以會利用 CAS 嘗試修改 Mark Word,如果成功,則線程 B 獲取偏向鎖成功,此時 Mark Word 中的偏向鎖線程 id 為線程 B id1000002:

- 但如果失敗了,就說明當前環(huán)境可能存在鎖競爭,則需要執(zhí)行偏向鎖撤銷操作。等到全局安全點時,JVM 會暫停持有偏向鎖的線程 A,檢查線程 A 的狀態(tài),若線程 A狀態(tài)為不活躍或者已經(jīng)執(zhí)行完了同步代碼塊,則設(shè)置鎖對象為無鎖狀態(tài)(線程 ID 為空,偏向鎖 0 ,鎖標志位為01)重新偏向,同時恢復(fù)線程 A,繼續(xù)獲取偏向鎖。如果線程 A 的同步代碼塊還沒執(zhí)行完,則需要升級為輕量級鎖。
- 在升級為輕量級鎖之前,持有偏向鎖的線程 A是暫停的,JVM 首先會在線程 A 的棧中創(chuàng)建一個名為鎖記錄的空間(
Lock Record),用于存放鎖對象目前的 Mark Word 的拷貝,然后拷貝對象頭中的 Mark Word 到線程 A 的鎖記錄中(官方稱之為 Displaced Mark Word ),若拷貝成功,JVM 將使用 CAS 嘗試將對象頭重的 Mark Word 更新為指向線程 A 的Lock Record的指針,成功,線程 A 獲取輕量級鎖,此時 Mark Word 的鎖標志位為 00,指向鎖記錄的指針指向線程 A 的鎖記錄地址,如下圖:

- 對于其他線程而言,也會在棧幀中建立鎖記錄,存儲鎖對象目前的 Mark Word 的拷貝。也利用 CAS 嘗試將鎖對象的 Mark Word 更正指向自身線程的 Lock Record,如果成功,表明競爭到輕量級鎖,則執(zhí)行同步代碼塊。如果失敗,那么線程嘗試使用自旋的方式來等待持有輕量級鎖的線程釋放鎖。當然,它不會一直自旋下去,因為自旋的過程也會消耗 CPU,而是自旋一定的次數(shù),如果自旋了一定次數(shù)后還是失敗,則升級為重量級鎖,阻塞所有未獲取鎖的線程,等待釋放鎖后喚醒。
最后是,鎖升級過程的詳細流程(此圖來源于網(wǎng)上):

以上就是Java synchronized的鎖升級過程詳解的詳細內(nèi)容,更多關(guān)于Java synchronized鎖升級的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot面試突擊之過濾器和攔截器區(qū)別詳解
過濾器(Filter)和攔截器(Interceptor)都是基于?AOP(Aspect?Oriented?Programming,面向切面編程)思想實現(xiàn)的,用來解決項目中某一類問題的兩種“工具”,但二者有著明顯的差距,接下來我們一起來看2022-10-10
SpringBoot項目改為SpringCloud項目使用nacos作為注冊中心的方法
本文主要介紹了SpringBoot項目改為SpringCloud項目使用nacos作為注冊中心,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04
Java使用線程池實現(xiàn)socket編程的方法詳解
這篇文章主要為大家詳細介紹了Java使用線程池實現(xiàn)socket編程的方法,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-03-03
eclipse創(chuàng)建springboot項目的三種方式總結(jié)
這篇文章主要介紹了eclipse創(chuàng)建springboot項目的三種方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07
springboot內(nèi)置tomcat之NIO處理流程一覽
這篇文章主要介紹了springboot內(nèi)置tomcat之NIO處理流程,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12
Java concurrency集合之ConcurrentHashMap_動力節(jié)點Java學(xué)院整理
這篇文章主要介紹了Java concurrency集合之ConcurrentHashMap的相關(guān)資料,需要的朋友可以參考下2017-06-06

