1. 程式人生 > >jvm synchronized底層設計與優化

jvm synchronized底層設計與優化

通常來講synchronized被當做重量級鎖來使用,但其實它並不是一味地阻塞當前執行緒,而是通過鎖升級等方式進行了很多的優化。

一 重量級鎖(互斥同步或悲觀鎖)

最原始的,也是synchronized與生俱來的同步方式。 使用synchronized可以指定一個鎖物件,如果沒有指定物件就是用當前物件的例項(非static的普通方法)或者當前物件的class物件(被static修飾的方法)作為被指定的鎖物件。鎖物件的鎖資訊放在鎖物件的頭資訊裡,由於考慮到提高虛擬機器空間使用效率等原因,這部分空間會根據不同的鎖狀態儲存不同的鎖資訊。如下圖所示。標誌位、儲存內容都在物件頭資訊中一個叫MarkWord的地方。

儲存內容 標誌位
分代年齡、雜湊碼、偏向鎖開啟位 01(未鎖定)
輕量級鎖記錄指標 00(輕量級鎖定)
重量級鎖記錄指標 10(重量及鎖定)
無記錄 11(gc)
偏向執行緒ID、epoch、分代年齡、偏向鎖開啟位 01(可偏向)

執行synchronized程式碼時,先嚐試獲取鎖物件的鎖,如果當前物件鎖的計數器是0(表示當前無執行緒競爭)或者計數器不為0單執行緒是同一個執行緒(同一個執行緒重入鎖),則當前執行緒獲取鎖,將鎖計數器加一;若釋放鎖則減一;若獲取物件失敗則當前執行緒阻塞等待,直到另一個執行緒釋放。

二 輕量級鎖(樂觀鎖)

jdk1.6之後加入的新型鎖機制,由於大多數時候是非併發狀態,此時重量級鎖消耗資源高且無用,因此加入了輕量級鎖。 加鎖過程如下:若尚未被鎖定(狀態為01)則在當前執行緒的棧頻中建立一個Lock Record的鎖空間,用於copy鎖資訊,copy完成後,使用cas操作,將MarkWord中的儲存內容換成指標(指標執行棧頻中Lock Record位置),再將MarkWord的標誌位變為00。若cas操作失敗,則檢視是否MarkWord中指標是否已經指向本執行緒的棧頻,若是,則執行緒已經持有此物件的鎖,若否,則執行緒被搶佔,進入阻塞狀態。 解鎖過程如下:同樣使用cas操作,若指標依然指向本執行緒棧頻,則將Lock Record中的資訊複製到MarkWord中。若cas失敗則存在其他併發執行緒,需釋放鎖的同時喚醒被阻塞的執行緒。 輕量級鎖適用於基本上不存在競爭的情況。一旦存在競爭不僅導致當前執行緒進入阻塞狀態,並且還有額外的cas開銷。

三 偏向鎖

jdk1.6之後加入的新型鎖機制,如果說輕量級鎖防止了非併發情況下的阻塞操作,那麼偏向鎖就是去掉了單執行緒操作下cas操作。偏向鎖很適合初始化時,在迴圈中重複訪問的需加鎖的程式碼。如建立ConcurrentHashMap後迴圈add操作。 所謂偏向鎖,就是加鎖程式碼在第一次被執行緒訪問時在MarkWord中記錄下執行緒id,如果以後還是這個執行緒在沒有併發的情況下訪問了,就可以進入程式碼塊,如果有其他執行緒進入程式碼塊,會觸發鎖升級即,偏向鎖變為輕量級鎖。偏向鎖關閉後不會再進入。 下圖是偏向鎖、輕量級鎖、重量級鎖轉換模式。 偏向鎖、輕量級鎖、重量級鎖轉換模式

四 自旋鎖與自適應鎖

jdk1.4.2之後引入的鎖機制,在輕量級鎖晉升到重量級鎖時可以加入自旋鎖或者自適應鎖。 由於阻塞操作佔用時間較長,有時甚至比同步程式碼更加耗費資源,所以使用多次迴圈使用cas嘗試獲取鎖的方式,來減少併發狀況下阻塞的發生。如果成功獲取則跳出迴圈,進入普通輕量級鎖模式;如果失敗則繼續迴圈,直到次數用盡,阻塞當前執行緒。 自旋鎖迴圈次數預設是10次。但jdk1.6開始,迴圈鎖升級為自適應鎖,迴圈次數由虛擬機器掌控。如果上次自旋獲取同步程式碼的鎖的時間較短,這次自旋時間就會延長,因為虛擬機器認為這段程式碼執行很快,使用自旋很容易獲取到鎖;相反,就會縮短自旋時間,甚至會取消自旋,以節省消耗。

五 鎖粗化與鎖消除

鎖粗化,當synchronized程式碼塊被寫在一個迴圈裡,重複的被不停加鎖解鎖,會讓效能受損,虛擬機器會將鎖範圍粗化到迴圈體外邊。 鎖消除,當synchronized程式碼塊被用於一個根本不會發生併發的地方,比如在一個方法裡建立StringBuffer並使用append方法。虛擬機器執行時會啟用逃逸分析,消除鎖。 鎖粗化與鎖消除體現了虛擬機器強大的一面,但不應該是程式設計師寫出低質量程式碼的藉口。如上述情況應使用非併發的StringBuider,而並非併發類StringBuffer。