偏向鎖,偏向線程id ,自旋鎖

理解鎖的基礎知識

若是想要透徹的理解Java鎖的前因後果,須要先了解如下基礎知識。java

基礎知識之一:鎖的類型

鎖從宏觀上分類,分爲悲觀鎖與樂觀鎖。linux

樂觀鎖

樂觀鎖是一種樂觀思想,即認爲讀多寫少,遇到併發寫的可能性低,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,採起在寫時先讀出當前版本號,而後加鎖操做(比較跟上一次的版本號,若是同樣則更新),若是失敗則要重複讀-比較-寫的操做。安全

java中的樂觀鎖基本都是經過CAS操做實現的,CAS是一種更新的原子操做,比較當前值跟傳入值是否同樣,同樣則更新,不然失敗。數據結構

悲觀鎖

悲觀鎖是就是悲觀思想,即認爲寫多,遇到併發寫的可能性高,每次去拿數據的時候都認爲別人會修改,因此每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會block直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嚐試cas樂觀鎖去獲取鎖,獲取不到,纔會轉換爲悲觀鎖,如RetreenLock。多線程

基礎知識之二:java線程阻塞的代價

java的線程是映射到操做系統原生線程之上的,若是要阻塞或喚醒一個線程就須要操做系統介入,須要在戶態與核心態之間切換,這種切換會消耗大量的系統資源,由於用戶態與內核態都有各自專用的內存空間,專用的寄存器等,用戶態切換至內核態須要傳遞給許多變量、參數給內核,內核也須要保護好用戶態在切換時的一些寄存器值、變量等,以便內核態調用結束後切換回用戶態繼續工做。併發

  1. 若是線程狀態切換是一個高頻操做時,這將會消耗不少CPU處理時間;
  2. 若是對於那些須要同步的簡單的代碼塊,獲取鎖掛起操做消耗的時間比用戶代碼執行的時間還要長,這種同步策略顯然很是糟糕的。

synchronized會致使爭用不到鎖的線程進入阻塞狀態,因此說它是java語言中一個重量級的同步操縱,被稱爲重量級鎖,爲了緩解上述性能問題,JVM從1.5開始,引入了輕量鎖與偏向鎖,默認啓用了自旋鎖,他們都屬於樂觀鎖。框架

明確java線程切換的代價,是理解java中各類鎖的優缺點的基礎之一。jvm

基礎知識之三:markword

在介紹java鎖以前,先說下什麼是markword,markword是java對象數據結構中的一部分,要詳細瞭解java對象的結構能夠點擊這裏,這裏只作markword的詳細介紹,由於對象的markword和java各類類型的鎖密切相關;函數

markword數據的長度在32位和64位的虛擬機(未開啓壓縮指針)中分別爲32bit和64bit,它的最後2bit是鎖狀態標誌位,用來標記當前對象的狀態,對象的所處的狀態,決定了markword存儲的內容,以下表所示:高併發

狀態 標誌位 存儲內容
未鎖定 01 對象哈希碼、對象分代年齡
輕量級鎖定 00 指向鎖記錄的指針
膨脹(重量級鎖定) 10 執行重量級鎖定的指針
GC標記 11 空(不須要記錄信息)
可偏向 01 偏向線程ID、偏向時間戳、對象分代年齡

32位虛擬機在不一樣狀態下markword結構以下圖所示:

這裏寫圖片描述

瞭解了markword結構,有助於後面瞭解java鎖的加鎖解鎖過程;

小結

前面提到了java的4種鎖,他們分別是重量級鎖、自旋鎖、輕量級鎖和偏向鎖, 
不一樣的鎖有不一樣特色,每種鎖只有在其特定的場景下,纔會有出色的表現,java中沒有哪一種鎖可以在全部狀況下都能有出色的效率,引入這麼多鎖的緣由就是爲了應對不一樣的狀況;

前面講到了重量級鎖是悲觀鎖的一種,自旋鎖、輕量級鎖與偏向鎖屬於樂觀鎖,因此如今你就可以大體理解了他們的適用範圍,可是具體如何使用這幾種鎖呢,就要看後面的具體分析他們的特性;

 

 

重量級鎖Synchronized

  

 

 

   

它有多個隊列,當多個線程一塊兒訪問某個對象監視器的時候,對象監視器會將這些線程存儲在不一樣的容器中。

  1. Contention List:競爭隊列,全部請求鎖的線程首先被放在這個競爭隊列中;

  2. Entry List:Contention List中那些有資格成爲候選資源的線程被移動到Entry List中;

  3. Wait Set:哪些調用wait方法被阻塞的線程被放置在這裏;

  4. OnDeck:任意時刻,最多隻有一個線程正在競爭鎖資源,該線程被成爲OnDeck;

  5. Owner:當前已經獲取到所資源的線程被稱爲Owner;

  6. !Owner:當前釋放鎖的線程。

JVM每次從隊列的尾部取出一個數據用於鎖競爭候選者(OnDeck),可是併發狀況下,ContentionList會被大量的併發線程進行CAS訪問,爲了下降對尾部元素的競爭,JVM會將一部分線程移動到EntryList中做爲候選競爭線程。Owner線程會在unlock時,將ContentionList中的部分線程遷移到EntryList中,並指定EntryList中的某個線程爲OnDeck線程(通常是最早進去的那個線程)。Owner線程並不直接把鎖傳遞給OnDeck線程,而是把鎖競爭的權利交給OnDeck,OnDeck須要從新競爭鎖。這樣雖然犧牲了一些公平性,可是能極大的提高系統的吞吐量,在JVM中,也把這種選擇行爲稱之爲「競爭切換」。

OnDeck線程獲取到鎖資源後會變爲Owner線程,而沒有獲得鎖資源的仍然停留在EntryList中。若是Owner線程被wait方法阻塞,則轉移到WaitSet隊列中,直到某個時刻經過notify或者notifyAll喚醒,會從新進去EntryList中。

處於ContentionList、EntryList、WaitSet中的線程都處於阻塞狀態,該阻塞是由操做系統來完成的(Linux內核下采用pthread_mutex_lock內核函數實現的)。

Synchronized是非公平鎖。 Synchronized在線程進入ContentionList時,等待的線程會先嚐試自旋獲取鎖,若是獲取不到就進入ContentionList,這明顯對於已經進入隊列的線程是不公平的,還有一個不公平的事情就是自旋獲取鎖的線程還可能直接搶佔OnDeck線程的鎖資源。

 

 

 

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

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

重量鎖在多線程下會致使線程阻塞;

可是阻塞或者喚醒一個線程時,都須要操做系統來幫忙,這就須要從用戶態轉換到內核態,而轉換狀態是須要消耗不少時間的,有可能比用戶執行代碼的時間還要長。
這就是說爲何重量級線程開銷很大的。

 

 

 

下面我講一步一步講解synchronized是如何被優化的,是如何從偏向鎖到重量級鎖的。

 

 

偏向鎖

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

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

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

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

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

此時偏向鎖的狀態爲「1」,說明對象的偏向鎖生效了,

總結下偏向鎖的步驟:

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

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

    

 

 

始終只有一個線程在執行同步塊 在有鎖的競爭時,偏向鎖會多作不少額外操做,尤爲是撤銷偏向所的時候會致使進入安全點,安全點會致使stw,致使性能降低,這種狀況下應當禁用;

查看停頓–安全點停頓日誌

要查看安全點停頓,能夠打開安全點日誌,經過設置JVM參數 -XX:+PrintGCApplicationStoppedTime 會打出系統中止的時間,添加-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 這兩個參數會打印出詳細信息,能夠查看到使用偏向鎖致使的停頓,時間很是短暫,可是爭用嚴重的狀況下,停頓次數也會很是多;

注意:安全點日誌不能一直打開: 
1. 安全點日誌默認輸出到stdout,一是stdout日誌的整潔性,二是stdout所重定向的文件若是不在/dev/shm,可能被鎖。 
2. 對於一些很短的停頓,好比取消偏向鎖,打印的消耗比停頓自己還大。 
3. 安全點日誌是在安全點內打印的,自己加大了安全點的停頓時間。

因此安全日誌應該只在問題排查時打開。 
若是在生產系統上要打開,再再增長下面四個參數: 
-XX:+UnlockDiagnosticVMOptions -XX: -DisplayVMOutput -XX:+LogVMOutput -XX:LogFile=/dev/shm/vm.log 
打開Diagnostic(只是開放了更多的flag可選,不會主動激活某個flag),關掉輸出VM日誌到stdout,輸出到獨立文件,/dev/shm目錄(內存文件系統)。

這裏寫圖片描述

此日誌分三部分: 
第一部分是時間戳,VM Operation的類型 
第二部分是線程概況,被中括號括起來 
total: 安全點裏的總線程數 
initially_running: 安全點時開始時正在運行狀態的線程數 
wait_to_block: 在VM Operation開始前須要等待其暫停的線程數

第三部分是到達安全點時的各個階段以及執行操做所花的時間,其中最重要的是vmop

  • spin: 等待線程響應safepoint號召的時間;
  • block: 暫停全部線程所用的時間;
  • sync: 等於 spin+block,這是從開始到進入安全點所耗的時間,可用於判斷進入安全點耗時;
  • cleanup: 清理所用時間;
  • vmop: 真正執行VM Operation的時間。

可見,那些不少但又很短的安全點,全都是RevokeBias, 高併發的應用會禁用掉偏向鎖。

jvm開啓/關閉偏向鎖

    • 開啓偏向鎖:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
    • 關閉偏向鎖:-XX:-UseBiasedLocking

 

 

鎖膨脹

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

鎖撤銷

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

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

輕量級鎖

 

在代碼進入同步塊的時候,若是此同步對象沒有被鎖定(鎖標誌位爲「01」狀態),虛擬機首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的MarkWord的拷貝(官方把這份拷貝加了一個Displaced前綴,即Displaced Mark Word),這時候線程堆棧與對象頭的狀態如圖13-3所示。


而後,虛擬機將使用CAS操做嘗試將對象的Mark Word更新爲指向Lock Record的指針。若是這個更新動做成功了,那麼這個線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌位(Mark Word的最後2bit)將轉變爲「00」,即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態如圖13-4所示。

若是這個更新操做失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,若是隻說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行,不然說明這個鎖對象已經被其餘線程搶佔了。若是有兩條以上的線程爭用同一個鎖,那輕量級鎖就再也不有效,要膨脹爲重量級鎖,鎖標誌的狀態值變爲「10」,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。

上面描述的是輕量級鎖的加鎖過程,它的解鎖過程也是經過CAS操做來進行的,若是對象的Mark Word仍然指向着線程的鎖記錄,那就用CAS操做把對象當前的Mark Word和線程中複製的Displaced Mark Word替換回來,若是替換成功,整個同步過程就完成了。若是替換失敗,說明有其餘線程嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的線程。

輕量級鎖能提高程序同步性能的依據是「對於絕大部分的鎖,在整個同步週期內都是不存在競爭的」,這是一個經驗數據。若是沒有競爭,輕量級鎖使用CAS操做避免了使用互斥量的開銷,但若是存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操做,所以在有競爭的狀況下,輕量級鎖會比傳統的重量級鎖更慢。

 

 

 

 總結偏向鎖撤銷之後升級爲輕量級鎖的過程:

  1. 線程在本身的棧楨中建立鎖記錄 LockRecord。
  2. 將鎖對象的對象頭中的MarkWord複製到線程的剛剛建立的鎖記錄中。
  3. 將鎖記錄中的Owner指針指向鎖對象。
  4. 將鎖對象的對象頭的MarkWord替換爲指向鎖記錄的指針對應的圖描述以下(圖來自周志明深刻java虛擬機)



輕量級鎖主要有兩種

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

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

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

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

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

自適應自旋鎖

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

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

 

 

 

 

鎖優勢的比較

相關文章
相關標籤/搜索