synchronized鎖升級的過程(偏向鎖到輕量鎖再到重量級鎖)轉

出處:完全搞懂synchronized(從偏向鎖到重量級鎖)html

 

 

接觸過線程安全的同窗想必都使用過synchronized這個關鍵字,在java同步代碼快中,synchronized的使用方式無非有兩個:java

  1. 經過對一個對象進行加鎖來實現同步,以下面代碼。
synchronized(lockObject){ //代碼
}
  1. 對一個方法進行synchronized聲明,進而對一個方法進行加鎖來實現同步。以下面代碼
public synchornized void test(){ //代碼
}

但這裏須要指出的是,不管是對一個對象進行加鎖仍是對一個方法進行加鎖,實際上,都是對對象進行加鎖編程

 

也就是說,對於方式2,實際上虛擬機會根據synchronized修飾的是實例方法仍是類方法,去取對應的實例對象或者Class對象來進行加鎖。數組

 

對於synchronized這個關鍵字,可能以前你們有聽過,他是一個重量級鎖,開銷很大,建議你們少用點。但你們可能也據說過,但到了jdk1.6以後,該關鍵字被進行了不少的優化,已經不像之前那樣不給力了,建議你們多使用。安全

 

那麼它是進行了什麼樣的優化,才使得synchronized又深得人心呢?爲什麼重量級鎖開銷就大呢?併發

想必你們也都據說太輕量級鎖,重量級鎖,自旋鎖,自適應自旋鎖,偏向鎖等等,他們都有哪些區別呢?
剛纔和你們說,鎖是加在對象上的,那麼一個線程是如何知道這個對象被加了鎖呢?又是如何知道它加的是什麼類型的鎖呢?
oracle

基於這些問題,下面我講一步一步講解synchronized是如何被優化的,是如何從偏向鎖到重量級鎖的。ide

鎖對象

剛纔咱們說,鎖其實是加在對象上的,那麼被加了鎖的對象咱們稱之爲鎖對象,在java中,任何一個對象都能成爲鎖對象。
爲了讓你們更好着理解虛擬機是如何知道這個對象就是一個鎖對象的,咱們下面簡單介紹一下java中一個對象的結構。
java對象在內存中的存儲結構主要有一下三個部分:

oop

  1. 對象頭
  2. 實例數據
  3. 填充數據
    這裏強調一下,對象頭裏的數據主要是一些運行時的數據。
    其簡單的結構以下

長度 內容 說明
32/64bit Mark Work hashCode,GC分代年齡,鎖信息
32/64bit Class Metadata Address 指向對象類型數據的指針
32/64bit Array Length 數組的長度(當對象爲數組時)

從該表格中咱們能夠看到,對象中關於鎖的信息是存在Markword裏的。
咱們來看一段代碼
優化

LockObject lockObject = new LockObject();//隨便建立一個對象
synchronized(lockObject){ //代碼
}

當咱們建立一個對象LockObject時,該對象的部分Markword關鍵數據以下。

bit fields 是否偏向鎖 鎖標誌位
hash 0 01

  從圖中能夠看出,偏向鎖的標誌位是「01」,狀態是「0」,表示該對象尚未被加上偏向鎖。(「1」是表示被加上偏向鎖)。該對象被建立出來的那一刻,就有了偏向鎖的標誌位,這也說明了全部對象都是可偏向的,但全部對象的狀態都爲「0」,也同時說明全部被建立的對象的偏向鎖並無生效。

偏向鎖

不過,當線程執行到臨界區(critical section)時,此時會利用CAS(Compare and Swap)操做,將線程ID插入到Markword中,同時修改偏向鎖的標誌位。

所謂臨界區,就是隻容許一個線程進去執行操做的區域,即同步代碼塊。CAS是一個原子性操做

此時的Mark word的結構信息以下:

bit fields   是否偏向鎖 鎖標誌位
threadId epoch 1 01

此時偏向鎖的狀態爲「1」,說明對象的偏向鎖生效了,同時也能夠看到,哪一個線程得到了該對象的鎖。

那麼,什麼是偏向鎖?

  偏向鎖是jdk1.6引入的一項鎖優化,其中的「偏」是偏愛的偏。它的意思就是說,這個鎖會偏向於第一個得到它的線程,在接下來的執行過程當中,假如該鎖沒有被其餘線程所獲取,沒有其餘線程來競爭該鎖,那麼持有偏向鎖的線程將永遠不須要進行同步操做。
也就是說:
在此線程以後的執行過程當中,若是再次進入或者退出同一段同步塊代碼,並再也不須要去進行加鎖或者解鎖操做,而是會作如下的步驟:

  1. Load-and-test,也就是簡單判斷一下當前線程id是否與Markword當中的線程id是否一致.
  2. 若是一致,則說明此線程已經成功得到了鎖,繼續執行下面的代碼.
  3. 若是不一致,則要檢查一下對象是否仍是可偏向,即「是否偏向鎖」標誌位的值。
  4. 若是還未偏向,則利用CAS操做來競爭鎖,也便是第一次獲取鎖時的操做。

  若是此對象已經偏向了,而且不是偏向本身,則說明存在了競爭。此時可能就要根據另外線程的狀況,多是從新偏向,也有多是作偏向撤銷,但大部分狀況下就是升級成輕量級鎖了。
  能夠看出,偏向鎖是針對於一個線程而言的,線程得到鎖以後就不會再有解鎖等操做了,這樣能夠省略不少開銷。假若有兩個線程來競爭該鎖話,那麼偏向鎖就失效了,進而升級成輕量級鎖了。
  爲何要這樣作呢?由於經驗代表,其實大部分狀況下,都會是同一個線程進入同一塊同步代碼塊的。這也是爲何會有偏向鎖出現的緣由。
  在Jdk1.6中,偏向鎖的開關是默認開啓的,適用於只有一個線程訪問同步塊的場景。


鎖膨脹

  剛纔說了,當出現有兩個線程來競爭鎖的話,那麼偏向鎖就失效了,此時鎖就會膨脹,升級爲輕量級鎖。這也是咱們常常所說的鎖膨脹

鎖撤銷

因爲偏向鎖失效了,那麼接下來就得把該鎖撤銷,鎖撤銷的開銷花費仍是挺大的,其大概的過程以下:

  1. 在一個安全點中止擁有鎖的線程。
  2. 遍歷線程棧,若是存在鎖記錄的話,須要修復鎖記錄和Markword,使其變成無鎖狀態。
  3. 喚醒當前線程,將當前鎖升級成輕量級鎖。
    因此,若是某些同步代碼塊大多數狀況下都是有兩個及以上的線程競爭的話,那麼偏向鎖就會是一種累贅,對於這種狀況,咱們能夠一開始就把偏向鎖這個默認功能給關閉

輕量級鎖

鎖撤銷升級爲輕量級鎖以後,那麼對象的Markword也會進行相應的的變化。下面先簡單描述下鎖撤銷以後,升級爲輕量級鎖的過程:

  1. 線程在本身的棧楨中建立鎖記錄 LockRecord。
  2. 將鎖對象的對象頭中的MarkWord複製到線程的剛剛建立的鎖記錄中。
  3. 將鎖記錄中的Owner指針指向鎖對象。
  4. 將鎖對象的對象頭的MarkWord替換爲指向鎖記錄的指針。

對應的圖描述以下(圖來自周志明深刻java虛擬機)
圖片1
圖片2

以後Markwork以下:

bit fields 鎖標誌位
指向LockRecord的指針 00

注:鎖標誌位」00」表示輕量級鎖
輕量級鎖主要有兩種

  1. 自旋鎖
  2. 自適應自旋鎖
自旋鎖

所謂自旋,就是指當有另一個線程來競爭鎖時,這個線程會在原地循環等待,而不是把該線程給阻塞,直到那個得到鎖的線程釋放鎖以後,這個線程就能夠立刻得到鎖的。
注意,鎖在原地循環的時候,是會消耗cpu的,就至關於在執行一個啥也沒有的for循環。
因此,輕量級鎖適用於那些同步代碼塊執行的很快的場景,這樣,線程原地等待很短很短的時間就可以得到鎖了。
經驗代表,大部分同步代碼塊執行的時間都是很短很短的,也正是基於這個緣由,纔有了輕量級鎖這麼個東西。


自旋鎖的一些問題
  1. 若是同步代碼塊執行的很慢,須要消耗大量的時間,那麼這個時侯,其餘線程在原地等待空消耗cpu,這會讓人很難受。
  2. 原本一個線程把鎖釋放以後,當前線程是可以得到鎖的,可是假如這個時候有好幾個線程都在競爭這個鎖的話,那麼有可能當前線程會獲取不到鎖,還得原地等待繼續空循環消耗cup,甚至有可能一直獲取不到鎖。

基於這個問題,咱們必須給線程空循環設置一個次數,當線程超過了這個次數,咱們就認爲,繼續使用自旋鎖就不適合了,此時鎖會再次膨脹,升級爲重量級鎖
默認狀況下,自旋的次數爲10次,用戶能夠經過-XX:PreBlockSpin來進行更改。

自旋鎖是在JDK1.4.2的時候引入的

自適應自旋鎖

所謂自適應自旋鎖就是線程空循環等待的自旋次數並不是是固定的,而是會動態着根據實際狀況來改變自旋等待的次數。
其大概原理是這樣的:
假如一個線程1剛剛成功得到一個鎖,當它把鎖釋放了以後,線程2得到該鎖,而且線程2在運行的過程當中,此時線程1又想來得到該鎖了,但線程2尚未釋放該鎖,因此線程1只能自旋等待,可是虛擬機認爲,因爲線程1剛剛得到過該鎖,那麼虛擬機以爲線程1此次自旋也是頗有可能可以再次成功得到該鎖的,因此會延長線程1自旋的次數
另外,若是對於某一個鎖,一個線程自旋以後,不多成功得到該鎖,那麼之後這個線程要獲取該鎖時,是有可能直接忽略掉自旋過程,直接升級爲重量級鎖的,以避免空循環等待浪費資源。


輕量級鎖也被稱爲非阻塞同步樂觀鎖,由於這個過程並無把線程阻塞掛起,而是讓線程空循環等待,串行執行。

重量級鎖

輕量級鎖膨脹以後,就升級爲重量級鎖了。重量級鎖是依賴對象內部的monitor鎖來實現的,而monitor又依賴操做系統的MutexLock(互斥鎖)來實現的,因此重量級鎖也被成爲互斥鎖
當輕量級所通過鎖撤銷等步驟升級爲重量級鎖以後,它的Markword部分數據大致以下

bit fields 鎖標誌位
指向Mutex的指針 10
爲何說重量級鎖開銷大呢

主要是,當系統檢查到鎖是重量級鎖以後,會把等待想要得到鎖的線程進行阻塞,被阻塞的線程不會消耗cup。可是阻塞或者喚醒一個線程時,都須要操做系統來幫忙,這就須要從用戶態轉換到內核態,而轉換狀態是須要消耗不少時間的,有可能比用戶執行代碼的時間還要長。
這就是說爲何重量級線程開銷很大的。

互斥鎖(重量級鎖)也稱爲阻塞同步悲觀鎖

總結

經過上面的分析,咱們知道了爲何synchronized關鍵字爲什麼又深得人心,也知道了鎖的演變過程。
也就是說,synchronized關鍵字並不是一開始就該對象加上重量級鎖,也是從偏向鎖,輕量級鎖,再到重量級鎖的過程。
這個過程也告訴咱們,假如咱們一開始就知道某個同步代碼塊的競爭很激烈、很慢的話,那麼咱們一開始就應該使用重量級鎖了,從而省掉一些鎖轉換的開銷。

 

參考資料

  1. 深刻理解java虛擬機(JVM高級特性與最佳實踐)
  2. java併發編程
  3. Eliminating Synchronization Related Atomic Operations with Biased Locking and Bulk Rebiasing
相關文章
相關標籤/搜索