JVM學習記錄-線程安全與鎖優化(二)
前言
高效并發是程序員們寫代碼時一直所追求的,HotSpot虛擬機開發團隊也為此付出了很多努力,為了在線程之間更高效地共享數據,以及解決競爭問題,HotSpot開發團隊做出了各種鎖的優化技術常見的有:自適應自旋鎖(Adaptive Spinning)、鎖消除(Lock Elimination)、鎖粗化(Lock Coarsening)、輕量級鎖(Lightweight Locking)和偏向鎖(Biased Locking)等。
自旋鎖與自適應自旋
互斥同步對性能最大的影響是阻塞的實現,線程的掛起和恢復的操作時要消耗系統資源的,在并發時頻繁的掛起和恢復線程這會給系統帶來很大的壓力。在許多應用中共享數據的鎖定狀態只會持續很短的一段時間,這段時間可能比線程的掛起和恢復的時間還短,這樣切換線程的狀態是很不值得的。因此虛擬機開發團隊在JDK1.4.2中引入了自旋鎖,在并發執行一段代碼時,如果已經有線程獲得鎖,后面的線程不會被直接掛起,而是區執行一個空循環(自旋),在若干個空循環后,線程如果獲得了鎖,則繼續執行,若線程依然不能獲得鎖,才會被掛起。自旋次數默認是10次,可以使用-XX:PreBlockSpin來更改。
JDK1.6中引入了自適應鎖,意味著自旋的時間不再固定,而是有之前的自旋時間及鎖的擁有者狀態來決定,若上一次成功獲得鎖,那么這一次允許自旋更長時間,若這個線程很少獲得鎖,有可能就跳過自旋直接被掛起。
鎖消除
鎖消除指虛擬機在即時編譯時,通過對運行上下文的掃描,發現一些被要求同步的代碼,不可能存在共享數據競爭的鎖,這個時候就需要把這些鎖進行消除,這樣可以節省毫無意義的請求時間。很多時候同步措施并不是開發人員手動加上的,而是JVM在運行期間轉換時加上的。
如下代碼:
public String concatString(String str1,String str2,String str3){ return str1+str2+str3;
}
因為String類是不可變的,每次的連接操作都是生成新的字符串,在JDK1.5之前會轉換成StringBuffer對象的連續append()操作,在JDK1.5及以后的版本中會轉換成Stringbuilder對象的連續append()操作。
轉換后的代碼如下:
public String concatString(String str1,String str2,String str3){ StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(str1); stringBuilder.append(str2); stringBuilder.append(str3); return stringBuilder.toString(); }
每一個append()方法都有一個同步塊,鎖的是stringBuilder對象,但是stringBuilder對象是concatString()方法的局部變量,顯然不會被其他線程訪問,因此可以安全的消除這里鎖。
鎖粗化
在編碼時推薦同步塊的作用范圍盡量的小,這樣范圍小了,出現競爭時等待線程也能最快的拿到鎖,但是如果頻繁的加鎖和解鎖也是很消耗資源的,所以虛擬機開發團隊對這種情況下的鎖進行了粗化,就是說如果虛擬機探測到有這樣一串零碎的操作都對同一個對象加鎖,將會把加鎖的同步范圍粗化到整個操作序列的外部。例如上面的代碼中stringBuilder對象的每一個append()方法都有一個鎖,虛擬機會把鎖范圍擴展到第一個append()操作之前直到最后一個append()操作之后。
輕量級鎖
輕量級鎖
輕量級鎖是相對于操作系統互斥量的傳統“重量”鎖來說的。并不是來代替重量級鎖,而是指在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。
理解輕量級鎖要先理解對象的內存布局,對象頭分為兩部分:
第一部分
用于存儲對象自身的運行時數據,哈希碼、GC分代年齡等。這部分數據的長度在32位和64位的虛擬分別為32bit和64bit,官方稱它為“Mark Word”,它是實現輕量級鎖和偏向鎖的關鍵。
第二部分
用于存儲指向方法區對象類型數據的指針,如果是數組對象的話,還會有一個額外的部分用戶存儲數組長度。
對象頭信息是與對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率,Mark Word被設計成一個非固定的數據結構以便在極小的空間內存存儲盡量多的信息,它會根據對象的狀態復用自己的存儲空間。

輕量級鎖加鎖過程
- 在代碼進入同步塊的時候,如果此時同步對象沒有被鎖定(鎖定標志位“01”狀態),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用戶存儲鎖對象目前的Mark Word的拷貝。
- 然后虛擬機將使用CAS(Compare-And-Swap)操作嘗試將對象的Mark Word更新為指向Lock Record的指針。
- 如果這個更新操作成功了,那么這個線程就擁有了該對象的鎖,并且Mark Word的鎖標志變為“00”。
- 如果這個更新操作失敗了,虛擬機首先檢查對象的Mark Word是否指向當前線程的棧幀,如果指向了說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行,否則說明這個鎖對象已經被其他線程搶占了。如果有兩條以上的線程爭用同一個鎖,要膨脹為重量級鎖,鎖標志變為“10”,MarkWord中存儲的就是指向重量級鎖的指針,后面等待鎖的線程也要進入阻塞狀態。
輕量級鎖解鎖過程
- 如果對象的Mark Word仍然指向著線程的鎖記錄,那就用CAS操作把對象當前的Mark Word和線程中復制的Displaced Mark Word替換回來。
- 如果替換成功,整個同步過程就完成了。
- 如果同步替換失敗,說明有其他線程嘗試過獲取該鎖,那就要釋放鎖的同時,喚醒被掛起的線程。
總結輕量級鎖
對于絕大部分的鎖,在整個同步周期內都是不存在競爭的。但是如果存在鎖競爭,那么除了互斥量開銷外,還額外發生了CAS操作,會比重量級鎖更慢。
偏向鎖
偏向鎖的目的
消除數據在無競爭情況下的同步原語,進一步提高程序的運行性能。
偏向鎖定義
如果說輕量級鎖是消除數據在無競爭的情況下使用CAS操作區消除同步使用的互斥量,那偏向鎖就是在無競爭情況下把整個同步都消除掉,鏈CAS操作都不做了。
為什么叫偏向鎖?
偏向鎖的意思是這個鎖會偏向于第一個獲得它的線程,如果接下來的執行過程中,該鎖沒有被其他線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步。
偏向鎖的工作原理
假設當前虛擬機啟用了偏向鎖(-XX:+UseBiasedLocking),那么,當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標志位設為“01”,即偏向模式。同時使用CAS操作把獲取到這個鎖的線程的ID記錄在對象的Mark Word中,如果CAS操作把獲取到這個鎖的線程ID記錄在對象的Mark Word之中,如果CAS操作成功,持有偏向鎖的線程以后每次進入這個鎖相關的同步塊時,虛擬機都可以不再進行任何同步操作。
自動解除偏向鎖
當有另外一個線程去嘗試獲取這個鎖時,偏向模式就宣告結束。根據對象目前是否處于被鎖定的狀態,撤銷偏向后恢復到未鎖定(標志位“01”)或輕量級鎖定(標志位“00”)的狀態,后續的同步操作就如輕量級鎖那樣執行。
總結
其實JVM層面還有讀寫分離鎖,以及靠開發人員的代碼來實現減少鎖的持有時間,這些都是在進行鎖的優化。 本來想著ReentrantLock和Sychronized各寫一個代碼的例子呢,但是發現書上的例子,我運行起來成了死循環了。《深入理解Java虛擬機第二版》這本書第395頁的代碼例子,我的jdk是1.8版本,感興趣的讀者可以在自己的環境下試試,如果有運行著不是死循環的也可以告訴我一下。從寫第一篇記錄讀這本書的博客到現在為止,差不多正好兩個月,這本書確實是本好書,里面的知識有深度,適合已經有過一定Java基礎的人看,講解的也到位,只不過是該更新了,里面講的還是jdk1.7的內容,但是最基礎的東西還都是一樣的。
還有就是要感悟一下了,看這本書的目的一開始是為了面試,但是從第二章開始看,剛開始看的時候第一次把JVM的內存結構弄明白后,心里很是激動的,(因為以前總是不知道jvm的堆是什么jvm的棧是什么? ),后來就一直堅持下來了,如果說只看一遍,我感覺這本書里的內容我是啥也記不住也看不明白,這樣看明白了記錄下來了,印象也很深刻,里面以前一些模棱兩可的知識,也得到了確認。現在這本書挑著看(有些部分感覺有些偏冷門的內容就沒看,例如:程序編譯與代碼優化)也算是看完了,然后這周也開始投簡歷找工作了,只是這個時間段已經過了金三銀四了,可能不那么好找工作了,不過相信自己的努力不會白費的,加油,后續若有時間了會把落下的那幾章也看完了,接著我要開啟新的記錄(設計模式學習記錄)。
作者:紀莫
歡迎任何形式的轉載,但請務必注明出處。
限于本人水平,如果文章和代碼有表述不當之處,還請不吝賜教。
歡迎掃描二維碼關注公眾號:Jimoer
文章會同步到公眾號上面,大家一起成長,共同提升技術能力。
聲援博主:如果您覺得文章對您有幫助,可以點擊文章右下角【推薦】一下。
您的鼓勵是博主的最大動力!


浙公網安備 33010602011771號