深度分析:鎖升級過程和鎖狀態,看完這篇你就懂了!

1、前言

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

2、鎖的四種狀態

在 synchronized 最初的實現方式是 「阻塞或喚醒一個Java線程須要操做系統切換CPU狀態來完成,這種狀態切換須要耗費處理器時間,若是同步代碼塊中內容過於簡單,這種切換的時間可能比用戶代碼執行的時間還長」,這種方式就是 synchronized實現同步最初的方式,這也是當初開發者詬病的地方,這也是在JDK6之前 synchronized效率低下的緣由,JDK6中爲了減小得到鎖和釋放鎖帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」。安全

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

如圖所示:
工具

3、鎖狀態的思路以及特色

4、鎖對比

5、Synchronized鎖

synchronized 用的鎖是存在Java對象頭裏的,那麼什麼是對象頭呢?性能

5.1 Java 對象頭

咱們以 Hotspot 虛擬機爲例,Hopspot 對象頭主要包括兩部分數據:Mark Word(標記字段) 和 Klass Pointer(類型指針)優化

Mark Word:默認存儲對象的HashCode,分代年齡和鎖標誌位信息。這些信息都是與對象自身定義無關的數據,因此Mark Word被設計成一個非固定的數據結構以便在極小的空間內存存儲儘可能多的數據。它會根據對象的狀態複用本身的存儲空間,也就是說在運行期間Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化。spa

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、鎖的分類

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)。當後續線程嘗試獲取鎖時,發現被佔用的鎖是重量級鎖,則直接將本身掛起(而不是忙等),等待未來被喚醒。

重量級鎖是指當有一個線程獲取鎖以後,其他全部等待獲取該鎖的線程都會處於阻塞狀態。

簡言之,就是全部的控制權都交給了操做系統,由操做系統來負責線程間的調度和線程的狀態變動。而這樣會出現頻繁地對線程運行狀態的切換,線程的掛起和喚醒,從而消耗大量的系統資

5、總結

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

相關文章
相關標籤/搜索