1、重量級鎖java
上篇文章中向你們介紹了Synchronized的用法及其實現的原理。如今咱們應該知道,Synchronized是經過對象內部的一個叫作監視器鎖(monitor)來實現的。可是監視器鎖本質又是依賴於底層的操做系統的Mutex Lock來實現的。而操做系統實現線程之間的切換這就須要從用戶態轉換到核心態,這個成本很是高,狀態之間的轉換須要相對比較長的時間,這就是爲何Synchronized效率低的緣由。所以,這種依賴於操做系統Mutex Lock所實現的鎖咱們稱之爲「重量級鎖」。JDK中對Synchronized作的種種優化,其核心都是爲了減小這種重量級鎖的使用。JDK1.6之後,爲了減小得到鎖和釋放鎖所帶來的性能消耗,提升性能,引入了「輕量級鎖」和「偏向鎖」。安全
2、輕量級鎖 多線程
鎖的狀態總共有四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨着鎖的競爭,鎖能夠從偏向鎖升級到輕量級鎖,再升級的重量級鎖(可是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級)。JDK 1.6中默認是開啓偏向鎖和輕量級鎖的,咱們也能夠經過-XX:-UseBiasedLocking來禁用偏向鎖。鎖的狀態保存在對象的頭文件中,以32位的JDK爲例:app
鎖狀態性能 |
25 bit測試 |
4bit優化 |
1bitspa |
2bit操作系統 |
|
23bit線程 |
2bit |
是不是偏向鎖 |
鎖標誌位 |
||
輕量級鎖 |
指向棧中鎖記錄的指針 |
00 |
|||
重量級鎖 |
指向互斥量(重量級鎖)的指針 |
10 |
|||
GC標記 |
空 |
11 |
|||
偏向鎖 |
線程ID |
Epoch |
對象分代年齡 |
1 |
01 |
無鎖 |
對象的hashCode |
對象分代年齡 |
0 |
01 |
「輕量級」是相對於使用操做系統互斥量來實現的傳統鎖而言的。可是,首先須要強調一點的是,輕量級鎖並非用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用產生的性能消耗。在解釋輕量級鎖的執行過程以前,先明白一點,輕量級鎖所適應的場景是線程交替執行同步塊的狀況,若是存在同一時間訪問同一鎖的狀況,就會致使輕量級鎖膨脹爲重量級鎖。
1、輕量級鎖的加鎖過程
(1)在代碼進入同步塊的時候,若是同步對象鎖狀態爲無鎖狀態(鎖標誌位爲「01」狀態,是否爲偏向鎖爲「0」),虛擬機首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,官方稱之爲 Displaced Mark Word。這時候線程堆棧與對象頭的狀態如圖2.1所示。
(2)拷貝對象頭中的Mark Word複製到鎖記錄中。
(3)拷貝成功後,虛擬機將使用CAS操做嘗試將對象的Mark Word更新爲指向Lock Record的指針,並將Lock record裏的owner指針指向object mark word。若是更新成功,則執行步驟(3),不然執行步驟(4)。
(4)若是這個更新動做成功了,那麼這個線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌位設置爲「00」,即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態如圖2.2所示。
(5)若是這個更新操做失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,若是是就說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行。不然說明多個線程競爭鎖,輕量級鎖就要膨脹爲重量級鎖,鎖標誌的狀態值變爲「10」,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。 而當前線程便嘗試使用自旋來獲取鎖,自旋就是爲了避免讓線程阻塞,而採用循環去獲取鎖的過程。
2、輕量級鎖的解鎖過程:
(1)經過CAS操做嘗試把線程中複製的Displaced Mark Word對象替換當前的Mark Word。
(2)若是替換成功,整個同步過程就完成了。
(3)若是替換失敗,說明有其餘線程嘗試過獲取該鎖(此時鎖已膨脹),那就要在釋放鎖的同時,喚醒被掛起的線程。
3、偏向鎖
引入偏向鎖是爲了在無多線程競爭的狀況下儘可能減小沒必要要的輕量級鎖執行路徑,由於輕量級鎖的獲取及釋放依賴屢次CAS原子指令,而偏向鎖只須要在置換ThreadID的時候依賴一次CAS原子指令(因爲一旦出現多線程競爭的狀況就必須撤銷偏向鎖,因此偏向鎖的撤銷操做的性能損耗必須小於節省下來的CAS原子指令的性能消耗)。上面說過,輕量級鎖是爲了在線程交替執行同步塊時提升性能,而偏向鎖則是在只有一個線程執行同步塊時進一步提升性能。
1、偏向鎖獲取過程:
(1)訪問Mark Word中偏向鎖的標識是否設置成1,鎖標誌位是否爲01——確認爲可偏向狀態。
(2)若是爲可偏向狀態,則測試線程ID是否指向當前線程,若是是,進入步驟(5),不然進入步驟(3)。
(3)若是線程ID並未指向當前線程,則經過CAS操做競爭鎖。若是競爭成功,則將Mark Word中線程ID設置爲當前線程ID,而後執行(5);若是競爭失敗,執行(4)。
(4)若是CAS獲取偏向鎖失敗,則表示有競爭。當到達全局安全點(safepoint)時得到偏向鎖的線程被掛起,偏向鎖升級爲輕量級鎖,而後被阻塞在安全點的線程繼續往下執行同步代碼。
(5)執行同步代碼。
2、偏向鎖的釋放:
偏向鎖的撤銷在上述第四步驟中有提到。偏向鎖只有遇到其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖,線程不會主動去釋放偏向鎖。偏向鎖的撤銷,須要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態,撤銷偏向鎖後恢復到未鎖定(標誌位爲「01」)或輕量級鎖(標誌位爲「00」)的狀態。
3、重量級鎖、輕量級鎖和偏向鎖之間轉換
4、其餘優化
1、適應性自旋(Adaptive Spinning):從輕量級鎖獲取的流程中咱們知道,當線程在獲取輕量級鎖的過程當中執行CAS操做失敗時,是要經過自旋來獲取重量級鎖的。問題在於,自旋是須要消耗CPU的,若是一直獲取不到鎖的話,那該線程就一直處在自旋狀態,白白浪費CPU資源。解決這個問題最簡單的辦法就是指定自旋的次數,例如讓其循環10次,若是還沒獲取到鎖就進入阻塞狀態。可是JDK採用了更聰明的方式——適應性自旋,簡單來講就是線程若是自旋成功了,則下次自旋的次數會更多,若是自旋失敗了,則自旋的次數就會減小。
2、鎖粗化(Lock Coarsening):鎖粗化的概念應該比較好理解,就是將屢次鏈接在一塊兒的加鎖、解鎖操做合併爲一次,將多個連續的鎖擴展成一個範圍更大的鎖。舉個例子:
package com.paddx.test.string; public class StringBufferTest { StringBuffer stringBuffer = new StringBuffer(); public void append(){ stringBuffer.append("a"); stringBuffer.append("b"); stringBuffer.append("c"); } }
這裏每次調用stringBuffer.append方法都須要加鎖和解鎖,若是虛擬機檢測到有一系列連串的對同一個對象加鎖和解鎖操做,就會將其合併成一次範圍更大的加鎖和解鎖操做,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖。
3、鎖消除(Lock Elimination):鎖消除即刪除沒必要要的加鎖操做。根據代碼逃逸技術,若是判斷到一段代碼中,堆上的數據不會逃逸出當前線程,那麼能夠認爲這段代碼是線程安全的,沒必要要加鎖。看下面這段程序:
package com.paddx.test.concurrent; public class SynchronizedTest02 { public static void main(String[] args) { SynchronizedTest02 test02 = new SynchronizedTest02(); //啓動預熱 for (int i = 0; i < 10000; i++) { i++; } long start = System.currentTimeMillis(); for (int i = 0; i < 100000000; i++) { test02.append("abc", "def"); } System.out.println("Time=" + (System.currentTimeMillis() - start)); } public void append(String str1, String str2) { StringBuffer sb = new StringBuffer(); sb.append(str1).append(str2); } }
雖然StringBuffer的append是一個同步方法,可是這段程序中的StringBuffer屬於一個局部變量,而且不會從該方法中逃逸出去,因此其實這過程是線程安全的,能夠將鎖消除。下面是我本地執行的結果:
爲了儘可能減小其餘因素的影響,這裏禁用了偏向鎖(-XX:-UseBiasedLocking)。經過上面程序,能夠看出消除鎖之後性能仍是有比較大提高的。
5、總結
本文重點介紹了JDk中採用輕量級鎖和偏向鎖等對Synchronized的優化,可是這兩種鎖也不是徹底沒缺點的,好比競爭比較激烈的時候,不但沒法提高效率,反而會下降效率,由於多了一個鎖升級的過程,這個時候就須要經過-XX:-UseBiasedLocking來禁用偏向鎖。下面是這幾種鎖的對比:
鎖 |
優勢 |
缺點 |
適用場景 |
偏向鎖 |
加鎖和解鎖不須要額外的消耗,和執行非同步方法比僅存在納秒級的差距。 |
若是線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。 |
適用於只有一個線程訪問同步塊場景。 |
輕量級鎖 |
競爭的線程不會阻塞,提升了程序的響應速度。 |
若是始終得不到鎖競爭的線程使用自旋會消耗CPU。 |
追求響應時間。 同步塊執行速度很是快。 |
重量級鎖 |
線程競爭不使用自旋,不會消耗CPU。 |
線程阻塞,響應時間緩慢。 |
追求吞吐量。 同步塊執行速度較長。 |