1. 概述
今天說的是無鎖、偏向鎖、輕量級鎖、重量級鎖。要注意的是,這四種狀態都不是Java語言中的鎖,而是Jvm爲了提高鎖的獲取與釋放效率而做的優化(使用synchronized時)的鎖。在Java1.5之前synchronized是一個重量級鎖,Java 1.6對synchronized進行的各種優化後,synchronized並不會顯得那麼重了。
瞭解這個幾個鎖之前希望大家對自旋鎖,和java對象有一定了解,我就簡單介紹下。
1.1 自旋鎖
自旋鎖是指線程A需要獲取鎖,但該鎖已經被其他線程B佔用時,該線程A不會被掛起,而是在不停的試圖獲取鎖,不斷的消耗CPU的時間,雖然CPU的時間被消耗了,但是比起線程的下文(瞭解上下文的請參考:https://blog.csdn.net/qq_39470733/article/details/77837403)切換消耗的時間要少。這個時候使用自旋鎖還是划算的。 而偏向鎖、輕量鎖、重量鎖也是一個鎖圍繞着如何使得程序運行的更加“划算”而進行改變的。
1.2 對象頭
HotSpot虛擬機中,對象在內存中存儲的佈局可以分爲三塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。HotSpot虛擬機的對象頭(Object Header)包括兩部分信息,第一部分用於存儲對象自身的運行時數據, 如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等等,這部分數據的長度在32位和64位的虛擬機(暫 不考慮開啓壓縮指針的場景)中分別爲32個和64個Bits,官方稱它爲“Mark Word”。
在32位的HotSpot虛擬機 中對象未被鎖定的狀態下,Mark Word的32個Bits空間中的25Bits用於存儲對象哈希碼(HashCode),4Bits用於存儲對象分代年齡,2Bits用於存儲鎖標誌 位,1Bit固定爲0,在其他狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)下對象的存儲內容如下表所示。
2. 鎖
2.1 偏向鎖
大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,偏向鎖可以降低多次加鎖解鎖的開銷。偏向鎖會偏向於第一個獲得它的線程,如果在接下來的執行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要同步。大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,爲了讓線程獲得鎖的代價更低而引入了偏向鎖。當有另外一個線程去嘗試獲取這個鎖時,偏向模式就宣告結束。根據鎖對象目前是否處於被鎖定的狀態,撤銷偏向後恢復到未鎖定或輕量級鎖定狀態。
引入偏向鎖是爲了在無多線程競爭的情況下儘量減少不必要的輕量級鎖執行路徑,因爲輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(瞭解CAS請參考:https://blog.csdn.net/qq_39470733/article/details/106811673)。
例如:當只有一個線程去競爭鎖的時候,我們不需要阻塞,也不需要自旋,因爲只有一個線程在競爭,我們只要去判斷該偏向鎖中的ThreadID是否爲當前線程即可。如果是就執行同步代碼,不是就嘗試使用CAS修改ThreadID,修改成功執行同步代碼,不成功就將偏向鎖升級成輕量鎖。
2.2 輕量鎖
獲取輕量鎖的過程與偏向鎖不同,競爭鎖的線程首先需要拷貝對象頭中的Mark Word到幀棧的鎖記錄中。拷貝成功後使用CAS操作嘗試將對象的Mark Word更新爲指向當前線程的指針。如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖。如果更新失敗,那麼意味着有多個線程在競爭。
當競爭線程嘗試佔用輕量級鎖失敗多次之後(使用自旋)輕量級鎖就會膨脹爲重量級鎖,重量級線程指針指向競爭線程,競爭線程也會阻塞,等待輕量級線程釋放鎖後喚醒他。
2.3 重量鎖
重量級鎖的加鎖、解鎖過程和輕量級鎖差不多,區別是:競爭失敗後,線程阻塞,釋放鎖後,喚醒阻塞的線程,不使用自旋鎖,不會那麼消耗CPU,所以重量級鎖適合用在同步塊執行時間長的情況下。
2.4 三種鎖的區別及使用場景
- 偏向鎖:僅有一個線程進入臨界區
- 輕量級鎖:多個線程交替進入臨界區
- 重量級鎖:多個線程同時進入臨界區
如有披露或問題歡迎留言或者入羣探討