關於 鎖的四種狀態與鎖升級過程 圖文詳解

牧小農的夏天發表於2020-06-06

一、前言

鎖的狀態總共有四種,級別由低到高依次為:無鎖、偏向鎖、輕量級鎖、重量級鎖,這四種鎖狀態分別代表什麼,為什麼會有鎖升級?其實在 JDK 1.6之前,synchronized 還是一個重量級鎖,是一個效率比較低下的鎖,但是在JDK 1.6後,Jvm為了提高鎖的獲取與釋放效率對(synchronized )進行了優化,引入了 偏向鎖 和 輕量級鎖 ,從此以後鎖的狀態就有了四種(無鎖、偏向鎖、輕量級鎖、重量級鎖),並且四種狀態會隨著競爭的情況逐漸升級,而且是不可逆的過程,即不可降級,也就是說只能進行鎖升級(從低階別到高階別),不能鎖降級(高階別到低階別),意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。

二、鎖的四種狀態

synchronized 最初的實現方式是 “阻塞或喚醒一個Java執行緒需要作業系統切換CPU狀態來完成,這種狀態切換需要耗費處理器時間,如果同步程式碼塊中內容過於簡單,這種切換的時間可能比使用者程式碼執行的時間還長”,這種方式就是 synchronized實現同步最初的方式,這也是當初開發者詬病的地方,這也是在JDK6以前 synchronized效率低下的原因,JDK6中為了減少獲得鎖和釋放鎖帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”。

所以目前鎖狀態一種有四種,從級別由低到高依次是:無鎖、偏向鎖,輕量級鎖,重量級鎖,鎖狀態只能升級,不能降級

如圖所示:
在這裡插入圖片描述

三、鎖狀態的思路以及特點

鎖狀態 儲存內容 標誌位
無鎖 物件的hashCode、物件分代年齡、是否是偏向鎖(0) 01
偏向鎖 偏向執行緒ID、偏向時間戳、物件分代年齡、是否是偏向鎖(1) 01
輕量級鎖 指向棧中鎖記錄的指標 00
重量級鎖 指向互斥量的指標 11

四、鎖對比

優點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級的差距 如果執行緒間存在鎖競爭,會帶來額外的鎖撤銷的消耗 適用於只有一個執行緒訪問同步塊場景
輕量級鎖 競爭的執行緒不會阻塞,提高了程式的響應速度 如果始終得不到索競爭的執行緒,使用自旋會消耗CPU 追求響應速度,同步塊執行速度非常快
重量級鎖 執行緒競爭不使用自旋,不會消耗CPU 執行緒阻塞,響應時間緩慢 追求吞吐量,同步塊執行速度較慢

五、Synchronized鎖

synchronized 用的鎖是存在Java物件頭裡的,那麼什麼是物件頭呢?

5.1 Java 物件頭

我們以 Hotspot 虛擬機器為例,Hopspot 物件頭主要包括兩部分資料:Mark Word(標記欄位) 和 Klass Pointer(型別指標)

Mark Word:預設儲存物件的HashCode,分代年齡和鎖標誌位資訊。這些資訊都是與物件自身定義無關的資料,所以Mark Word被設計成一個非固定的資料結構以便在極小的空間記憶體儲存儘量多的資料。它會根據物件的狀態複用自己的儲存空間,也就是說在執行期間Mark Word裡儲存的資料會隨著鎖標誌位的變化而變化。

Klass Point:物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。

在上面中我們知道了,synchronized 用的鎖是存在Java物件頭裡的,那麼具體是存在物件頭哪裡呢?答案是:存在鎖物件的物件頭的Mark Word中,那麼MarkWord在物件頭中到底長什麼樣,它到底儲存了什麼呢?

在64位的虛擬機器中:
在這裡插入圖片描述
在32位的虛擬機器中:
在這裡插入圖片描述

下面我們以 32位虛擬機器為例,來看一下其 Mark Word 的位元組具體是如何分配的

無鎖:物件頭開闢 25bit 的空間用來儲存物件的 hashcode ,4bit 用於存放物件分代年齡,1bit 用來存放是否偏向鎖的標識位,2bit 用來存放鎖標識位為01

偏向鎖: 在偏向鎖中劃分更細,還是開闢 25bit 的空間,其中23bit 用來存放執行緒ID,2bit 用來存放 Epoch,4bit 存放物件分代年齡,1bit 存放是否偏向鎖標識, 0表示無鎖,1表示偏向鎖,鎖的標識位還是01

輕量級鎖:在輕量級鎖中直接開闢 30bit 的空間存放指向棧中鎖記錄的指標,2bit 存放鎖的標誌位,其標誌位為00

重量級鎖: 在重量級鎖中和輕量級鎖一樣,30bit 的空間用來存放指向重量級鎖的指標,2bit 存放鎖的標識位,為11

GC標記: 開闢30bit 的記憶體空間卻沒有佔用,2bit 空間存放鎖標誌位為11。

其中無鎖和偏向鎖的鎖標誌位都是01,只是在前面的1bit區分了這是無鎖狀態還是偏向鎖狀態

關於記憶體的分配,我們可以在git中openJDK中 markOop.hpp 可以看出:

public:
  // Constants
  enum { age_bits                 = 4,
         lock_bits                = 2,
         biased_lock_bits         = 1,
         max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
         hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits,
         cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
         epoch_bits               = 2
  };
  • age_bits: 就是我們說的分代回收的標識,佔用4位元組
  • lock_bits: 是鎖的標誌位,佔用2個位元組
  • biased_lock_bits: 是是否偏向鎖的標識,佔用1個位元組
  • max_hash_bits: 是針對無鎖計算的hashcode 佔用位元組數量,如果是32位虛擬機器,就是 32 - 4 - 2 -1 = 25 byte,如果是64 位虛擬機器,64 - 4 - 2 - 1 = 57 byte,但是會有 25 位元組未使用,所以64位的 hashcode 佔用 31 byte
  • hash_bits: 是針對 64 位虛擬機器來說,如果最大位元組數大於 31,則取31,否則取真實的位元組數
  • cms_bits: 不是64位虛擬機器就佔用 0 byte,是64位就佔用 1byte
  • epoch_bits: 就是 epoch 所佔用的位元組大小,2位元組。

5.2 Monitor

Monitor 可以理解為一個同步工具或一種同步機制,通常被描述為一個物件。每一個 Java 物件就有一把看不見的鎖,稱為內部鎖或者 Monitor 鎖。

Monitor 是執行緒私有的資料結構,每一個執行緒都有一個可用 monitor record 列表,同時還有一個全域性的可用列表。每一個被鎖住的物件都會和一個 monitor 關聯,同時 monitor 中有一個 Owner 欄位存放擁有該鎖的執行緒的唯一標識,表示該鎖被這個執行緒佔用。

Synchronized是通過物件內部的一個叫做監視器鎖(monitor)來實現的,監視器鎖本質又是依賴於底層的作業系統的 Mutex Lock(互斥鎖)來實現的。而作業系統實現執行緒之間的切換需要從使用者態轉換到核心態,這個成本非常高,狀態之間的轉換需要相對比較長的時間,這就是為什麼 Synchronized 效率低的原因。因此,這種依賴於作業系統 Mutex Lock 所實現的鎖我們稱之為重量級鎖。

隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級)。JDK 1.6中預設是開啟偏向鎖和輕量級鎖的,我們也可以通過-XX:-UseBiasedLocking=false來禁用偏向鎖。

六、鎖的分類

6.2 無鎖

無鎖是指沒有對資源進行鎖定,所有的執行緒都能訪問並修改同一個資源,但同時只有一個執行緒能修改成功。

無鎖的特點是修改操作會在迴圈內進行,執行緒會不斷的嘗試修改共享資源。如果沒有衝突就修改成功並退出,否則就會繼續迴圈嘗試。如果有多個執行緒修改同一個值,必定會有一個執行緒能修改成功,而其他修改失敗的執行緒會不斷重試直到修改成功。

6.3 偏向鎖

初次執行到synchronized程式碼塊的時候,鎖物件變成偏向鎖(通過CAS修改物件頭裡的鎖標誌位),字面意思是“偏向於第一個獲得它的執行緒”的鎖。執行完同步程式碼塊後,執行緒並不會主動釋放偏向鎖。當第二次到達同步程式碼塊時,執行緒會判斷此時持有鎖的執行緒是否就是自己(持有鎖的執行緒ID也在物件頭裡),如果是則正常往下執行。由於之前沒有釋放鎖,這裡也就不需要重新加鎖。如果自始至終使用鎖的執行緒只有一個,很明顯偏向鎖幾乎沒有額外開銷,效能極高。

偏向鎖是指當一段同步程式碼一直被同一個執行緒所訪問時,即不存在多個執行緒的競爭時,那麼該執行緒在後續訪問時便會自動獲得鎖,從而降低獲取鎖帶來的消耗,即提高效能。

當一個執行緒訪問同步程式碼塊並獲取鎖時,會在 Mark Word 裡儲存鎖偏向的執行緒 ID。線上程進入和退出同步塊時不再通過 CAS 操作來加鎖和解鎖,而是檢測 Mark Word 裡是否儲存著指向當前執行緒的偏向鎖。輕量級鎖的獲取及釋放依賴多次 CAS 原子指令,而偏向鎖只需要在置換 ThreadID 的時候依賴一次 CAS 原子指令即可。

偏向鎖只有遇到其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖,執行緒是不會主動釋放偏向鎖的。

關於偏向鎖的撤銷,需要等待全域性安全點,即在某個時間點上沒有位元組碼正在執行時,它會先暫停擁有偏向鎖的執行緒,然後判斷鎖物件是否處於被鎖定狀態。如果執行緒不處於活動狀態,則將物件頭設定成無鎖狀態,並撤銷偏向鎖,恢復到無鎖(標誌位為01)或輕量級鎖(標誌位為00)的狀態。

6.4 輕量級鎖(自旋鎖)

在這裡插入圖片描述

輕量級鎖是指當鎖是偏向鎖的時候,卻被另外的執行緒所訪問,此時偏向鎖就會升級為輕量級鎖,其他執行緒會通過自旋(關於自旋的介紹見文末)的形式嘗試獲取鎖,執行緒不會阻塞,從而提高效能。

輕量級鎖的獲取主要由兩種情況:
① 當關閉偏向鎖功能時;
② 由於多個執行緒競爭偏向鎖導致偏向鎖升級為輕量級鎖。

一旦有第二個執行緒加入鎖競爭,偏向鎖就升級為輕量級鎖(自旋鎖)。這裡要明確一下什麼是鎖競爭:如果多個執行緒輪流獲取一個鎖,但是每次獲取鎖的時候都很順利,沒有發生阻塞,那麼就不存在鎖競爭。只有當某執行緒嘗試獲取鎖的時候,發現該鎖已經被佔用,只能等待其釋放,這才發生了鎖競爭。

在輕量級鎖狀態下繼續鎖競爭,沒有搶到鎖的執行緒將自旋,即不停地迴圈判斷鎖是否能夠被成功獲取。獲取鎖的操作,其實就是通過CAS修改物件頭裡的鎖標誌位。先比較當前鎖標誌位是否為“釋放”,如果是則將其設定為“鎖定”,比較並設定是原子性發生的。這就算搶到鎖了,然後執行緒將當前鎖的持有者資訊修改為自己。

長時間的自旋操作是非常消耗資源的,一個執行緒持有鎖,其他執行緒就只能在原地空耗CPU,執行不了任何有效的任務,這種現象叫做忙等(busy-waiting)。如果多個執行緒用一個鎖,但是沒有發生鎖競爭,或者發生了很輕微的鎖競爭,那麼synchronized就用輕量級鎖,允許短時間的忙等現象。這是一種折衷的想法,短時間的忙等,換取執行緒在使用者態和核心態之間切換的開銷。

6.4 重量級鎖

重量級鎖顯然,此忙等是有限度的(有個計數器記錄自旋次數,預設允許迴圈10次,可以通過虛擬機器引數更改)。如果鎖競爭情況嚴重,某個達到最大自旋次數的執行緒,會將輕量級鎖升級為重量級鎖(依然是CAS修改鎖標誌位,但不修改持有鎖的執行緒ID)。當後續執行緒嘗試獲取鎖時,發現被佔用的鎖是重量級鎖,則直接將自己掛起(而不是忙等),等待將來被喚醒。

重量級鎖是指當有一個執行緒獲取鎖之後,其餘所有等待獲取該鎖的執行緒都會處於阻塞狀態。

簡言之,就是所有的控制權都交給了作業系統,由作業系統來負責執行緒間的排程和執行緒的狀態變更。而這樣會出現頻繁地對執行緒執行狀態的切換,執行緒的掛起和喚醒,從而消耗大量的系統資

五、總結

文中講述了鎖的四種狀態以及鎖是如何一步一步升級的過程,文中有理解不到位或者有問題的地方,歡迎大家在評論區中下方指出和交流,謝謝大家

相關文章