從JDK 5升級到JDK 6後實現各種鎖優化技術如下:

  • 適應性自旋(Adaptive Spinning)
  • 鎖消除(Lock Elimination)
  • 鎖膨脹(Lock Coarsening)
  • 輕量級鎖(Lightweight Locking)
  • 偏向鎖(Biased Locking)
(注:自旋鎖在4的時候就有了,不過4中默認不開啓,6中默認開啓)

在介紹各個鎖是什麼?解除了什麼問題?如何實現的?這三個問題前,先讓我們設想這麼一種場景前提:

通過你所處一個班級,這個班級呢有一個誰都能夠寫,但被鎖住的班級日記本。日記本鑰匙放在了老師辦公室且用完一定要歸還給老師。

自旋鎖

場景模擬

第一天,你想在日記本上寫些東西,於是你到老師辦公室去拿鑰匙,但是此時鑰匙已經被另一個同學拿去記錄了,你一時拿不到,
這時你想到一般大家寫日記都很快(在許多應用上,共享數據的鎖定狀態只會持續很短的時間)。回教室(線程掛起)然後等老師通知有鑰匙了(鎖釋放)再過來(線程恢復),中間要走很遠的路(性能消耗),不划算。
不如就在這等10s(自旋鎖:默認自旋10次你花費10s(就是),於佔用處理器時間)進行等待,最終在10s內成功獲取到日記,避免了來回跑的時間損失(線程切換)。

什麼?就是自旋鎖的作用

自旋鎖的作用就是讓請求鎖的線程“稍等一會”,在等待期間不放棄處理器的執行時間,看看持有鎖的線程會不會很快釋放鎖。

自旋鎖為什麼要這麼做?

自旋鎖之所以這麼做,是因為互 斥同步中對性能影響最大就是阻塞實現,線程掛起和恢復都要轉入內核態中完成,給併發性能帶來了很大的壓力。
同時,在許多應用上,共享數據的鎖定狀態只會持續很短的時間,為了這一小段時間去浪費性能不值得。

自旋鎖怎麼實現的?

自旋鎖是通過讓線程執行一個忙循環來實現的。
自旋鎖不能代替阻塞,自旋鎖即使避免了線程掛起恢復(線程切換)的開銷,但要求佔用處理器的時間。就是但其中有一個點,就
所以自旋等待時間一定要有限度,默認的自旋次數是10次,超過次數線程會通過傳統方式進行掛起。
自旋次數許可通過-XX:PreBlockSpin來自行更改。

自旋鎖許可引申適應性自旋。

通過自旋次數雖然能夠指定但仍然是一個固定的值,並不靈活,於是jdk6之後引入了適應性自旋的概念。

適應性自旋

第二天,你又想去寫日記了,但聰明的你想到,每天大家記錄的東西不一樣,記錄所花費的時間也不一樣,
所以你想如果上一位同學記錄需11s,而你只等10s就回班就很不划算。如果如果上一位同學記錄需要1000s,那其實能夠直接下次再來。
怎麼大致估算等待時間呢(鎖釋放時間)聰明的你於是想到,能夠詢問上一個同學等待的時間(上一個線程在同一個鎖上的自旋時間)。
假如他等到了日記本,那自己便大概率也可以等到,10秒等不到也可以適當多等一會,比如100s(上一次獲取到了,相應增加自旋次數)。
若是大家都沒等到,那便等老師通知有鑰匙了再過來(省略自旋來減少處理器資源的浪費)。

什麼?就是適應性自旋的作用

自旋等待次數由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定的,
上一獲取到的情況下,虛擬機便會認為再次獲取到的概率較大,會相應增加自旋次數。就是簡單講就
如果歷史上很少獲取到,虛擬機則可能省略自旋來減少處理器資源的浪費。
如此,程序運行時間越長,虛擬機的預測便會越準確。

鎖消除

場景模擬

第三天老師改了規則,老師決定筆記本只有你能寫,而你發現每次寫之前還得跑辦公室拿鑰匙開鎖很麻煩(線程切換),
這時你就想呀,老師制定的新規則下只有我能寫(逃逸分析:堆上的所有數據都不會被其他線程訪問到),
我把筆記本直接不鎖了不就好了(鎖消除)。

鎖消除?就是什麼

鎖消除是在一些代碼要求同步,但實際沒有共享數據競爭的情況下,忽略同步措施(對鎖進行消除)。

為什麼需要鎖消除?

鎖消除可以在不需要代碼中錯誤同步的位置減少線程切換的消耗。

鎖消除怎麼搭建的?

主要依賴於逃逸分析,如果判斷到一段代碼中,在堆上的所有數據都不會被其他線程訪問到,那就可以進行鎖消除。

鎖消除的例子:

假如你在一個線程安全代碼快中應用StringBuffer的對象進行字符串連接,
虛擬機會觀察該對象,經過逃逸分析後,發現這個對象的操作被限制在了該代碼塊中,那麼就會忽略所有同步措施。

逃逸分析?

鎖粗化

場景模擬

第四天,老師又把鑰匙收了回去。
你發現自己上午總是得拿三次鑰匙回來開鎖才能把話寫完(一連串操作都是對同一個對象反覆加鎖解鎖),
因此你就想呀,我不如一次性把三次想寫的東西都寫了(鎖粗化),省的來回跑拿鑰匙(頻繁互斥同步操作

鎖粗化?就是什麼

要是在循環體加鎖或者一連串同步操作都是對同一個對象進行,那麼細粒度鎖升級為粗粒度鎖。

為什麼要求鎖粗化?

説出現了頻繁互斥同步操作,導致了不必要的性能浪費。就是上面可以看到這一連串操作都是對同一個對象反覆加鎖解鎖,也就

鎖粗化怎麼實現的?

倘若虛擬機發現有一串零碎的操控都是對同一個對象加鎖,那就會把鎖同步範圍擴展到操作序列的外部。

輕量級鎖

場景模擬

第五天,老師發現來拿鑰匙的總是同一個人,於是同意在下一個人來拿鑰匙前(線程競爭通過),鑰匙能夠暫時留在這位同學這裏(輕量級鎖)。
於是這位同學在每次寫日誌前,不需要跑辦公室去拿鑰匙(線程掛起恢復),只需要拿鑰匙開一下箱子(CAS)即可。
只有另一個同學也來拿鑰匙時才需要把鑰匙歸還老師(升級重量級鎖:維護互斥量)。

輕量級鎖?就是什麼

通過CAS操作避免啓用互斥量產生的開銷。

為什麼需要輕量級鎖?

對於絕大部分鎖,在整個同步週期內都是不存在競爭的。

輕量級鎖是怎麼搭建的?

鎖對象上維護了一個鎖標誌位(01未鎖定 00 輕量級鎖 10 重量級鎖)

加鎖

首先,在代碼即將進入同步塊時,如果鎖標誌位為01(未鎖定),那麼虛擬機首先將在當前線程的棧幀中建立一個Lock Record(鎖記錄)空間,並存儲鎖對象目前的Mark Word拷貝。
然後,通過CAS操作(記錄在Lock Record中的值與鎖對象的Mark wod比較,相同則將鎖對象的Mark Word更新為線程的Lock Record空間的指針)
如果CAS成功,則將鎖標誌位更新為00(輕量級鎖定狀態)
如果CAS失敗,先會檢查鎖對象的Mark Word是否是當前線程的Lock Record指針。
是,則直接進入同步塊。
否,則説明至少2條線程在競爭鎖。此時則膨脹為重量級鎖,鎖標誌位更新為10(重量級鎖定狀態),
此時Mark Word更新為指向重量級鎖(互斥量)的指針。

解鎖

也是通過CAS,實際就是加鎖倒着來一遍。

CAS成功,則把線程 Lock Record空間內的指針複製回鎖對象的Mark Word

CAS失敗,則説明有其他線程嘗試獲取鎖,所以要在釋放的同時喚起鎖

jvm併發之鎖優化 - 僑居者 -_Word

jvm併發之鎖優化 - 僑居者 -_自旋鎖_02

擴展

並不是所有情況下輕量級鎖都會減少性能消耗,如果確實存在競爭,那麼輕量級鎖必將升級成重量級鎖,此種情況下,CAS的開銷就是額外產生的。

CAS(Compare-And-Swap)?

簡單講就是依據記錄在記錄在線程Lock Record中的值與鎖對象的Mark wod比較,相同則將鎖對象的Mark Word更新為線程的Lock Record空間的指針
依據這個操作來保證鎖對象只能被一個對象持有

偏向鎖

場景模擬

第六天,暫時保管鑰匙的同學發現每次還是要求拿鑰匙開一下箱子(CAS),於是在自己家裏拿來一個指紋鎖暫時替代之前的鎖(偏向模式:1),這樣就不用每次都拿鑰匙開鎖了(避免CAS的開銷)。
當有其他同學也想寫時,再把指紋鎖拿掉(取消偏向模式:0),換回之前的鑰匙鎖,並把鑰匙移交給另一個同學保管(升級為輕量級鎖)。

什麼是偏向鎖?

鎖對象會偏向第一個獲取他的線程,如果一直是當前線程重複進入鎖,沒有線程競爭,那麼偏向鎖的線程則不必須同步。

為什麼需偏向鎖?

偏向鎖可以提高帶有同步但無競爭的腳本性能
輕鬆講偏向鎖就是經過維護偏向狀態,來減少當前線程重複進入鎖帶來CAS操作性能消耗。

偏向鎖是怎麼實現的?

產生偏向鎖

線程第一次獲取鎖對象的時候,虛擬機會講鎖標誌位設置為01(輕量級鎖),同時把偏向模式設置為1(偏向模式)
同時進行輕量級鎖加鎖所產生的CAS操控(Mark Word -> Lock Record, 線程ID -> Mark Word)。
上訴CAS成功後,持有偏向鎖的線程每次進入這個鎖,都不再需要同步操作(加鎖,解鎖,Mark Word更新等)

結束偏向鎖

一旦出現,線程競爭,偏向鎖立刻結束,同時,偏向模式設置為0。鎖標誌位恢復成 01(未鎖定) 00 (輕量級鎖)。
後續像輕量級鎖那樣去執行。

jvm併發之鎖優化 - 僑居者 -_Word_03

拓展

如果程序中大多數的鎖都總是被多個不同的線程訪問,那偏向模式就是多餘的。禁止反而會帶來性能提升
應用參數-XX:-UseBiasedLocking可能禁止偏向鎖優化。