引言
HotSpot虛擬機團隊在1.5 -> 1.6版本演進中,進行了大量的鎖優(yōu)化技術,相應的jdk6并發(fā)包也推出了很多并發(fā)容器&API,所以JDK6是高效并發(fā)大放異彩的一個關鍵版本。本文主要介紹一下java虛擬機中對于鎖的優(yōu)化技術、逃逸分析技術。
鎖優(yōu)化:適應性自旋、鎖消除、鎖粗化、輕量級鎖和偏向鎖等
逃逸分析:棧上分配、同步消除、標量替換等
理論基礎
在進行鎖優(yōu)化介紹&逃逸分析介紹之前,先回顧一下以下基礎概念,是有必要的。
·synchronized同步方法基于ACC_SYNCHRONIZED關鍵字隱式對方法進行加鎖,同步代碼塊基于monitorenter&monitorexit進行加鎖與釋放鎖。在鎖優(yōu)化技術還未成熟之前,synchronized實現(xiàn)是直接通過ObjectMonitor調(diào)用enter&exit進行“重量級鎖”操作。
·對象頭中主要包含了GC分代年齡、鎖狀態(tài)標記、哈希碼、epoch等信息。對象的狀態(tài)一共有五種,分別是無鎖態(tài)、輕量級鎖、重量級鎖、GC標記和偏向鎖
·在HotSpot虛擬機中,使用oop-klass模型來表示對象

·線程五種基本狀態(tài),鎖與線程&線程狀態(tài)的切換息息相關。
對于線程來說,一共有五種狀態(tài),分別為:初始狀態(tài)(New) 、就緒狀態(tài)(Runnable) 、運行狀態(tài)(Running) 、阻塞狀態(tài)(Blocked) 和死亡狀態(tài)(Dead) 。

·java多線程模型
·使用內(nèi)核線程 1:1
·使用用戶線程 1:n
·使用用戶線程加輕量級進程混合實現(xiàn)? m&n
鎖優(yōu)化
自旋鎖
基于java多線程模型、線程狀態(tài)切換與互斥原理,可以得知對性能最大的影響是阻塞的實現(xiàn),掛起和恢復線程需要映射到OS內(nèi)核態(tài)中完成,同時,虛擬機團隊發(fā)現(xiàn)在許多應用上,共享數(shù)據(jù)的鎖定狀態(tài)只會持續(xù)很短一段時間,為了這段時間去掛起和恢復線程很沒有性價比。如果物理機有一個以上處理器,可以實現(xiàn)并行操作,那么我們就可以讓后面請求鎖的那個線程'稍等一下',但不放棄處理器的執(zhí)行時間,看看持有鎖的線程是否很快就會釋放鎖。這個“稍微等一下”的過程就是自旋。
自旋鎖在JDK1.4已經(jīng)引入進來,但是默認關閉,JDK6默認開啟。
在JDK1.4版本,可以通過參數(shù)-XX:UseSpinning選擇開啟自旋,-XX:PreBlockSpin來更改自旋的時間,默認值是10次。
自適應自旋鎖
JDK6引入了自適應的自旋鎖,于是我們不需要再固定指定一個自旋時間,虛擬機會有算法策略智能的選擇時間,隨著程序運行和性能監(jiān)控信息不斷完善,虛擬機的預測會越來越精準,越來越'聰明'。
自旋與阻塞的區(qū)別在于是否放棄處理器的執(zhí)行時間,自旋比較適合競爭不激烈,并且保持鎖的時間短的場景。
鎖消除
鎖消除是JIT編譯器優(yōu)化功能之一,也是逃逸分析下面要講到的。
顧名思義,就是JIT編譯同步代碼塊的時候,會使用逃逸分析技術來判斷當前代碼塊,是否是局部變量等,只會被一個線程訪問到。打個比方

在上面代碼塊中,StringBuffer內(nèi)部是synchronized修飾的,但是它在代碼塊中屬于局部變量,是線程私有的。還有例子2,也是局部變量,所以這種情況,JIT會幫我們進行優(yōu)化,進行鎖消除,加鎖完全沒有意義。
由于synchronized是基于moniotor指令實現(xiàn)的,可能有人就想javap試一下是不是真的鎖消除了,這里需要提一下,JIT編譯階段的優(yōu)化,javap無法查看具體結(jié)果,如果讀者感興趣,還是可以看的,只是會復雜一點,首先你要自己build一個fasttest版本的jdk,然后在使用java命令對.class文件進行執(zhí)行的時候加上-XX:+PrintEliminateLocks參數(shù)。而且jdk的模式還必須是server模式。
鎖粗化
鎖的細化老生常談的問題了,在使用鎖的時候,控制粒度有利于提升性能,在真正競態(tài)條件發(fā)生的地區(qū)才用鎖。
那為什么會有鎖粗化這一說呢,很簡單,同樣的道理我們一定有經(jīng)驗且碰到過。
在你寫一段try catch異常處理的時候,如果代碼塊中有循環(huán)操作,你會把try catch寫在循環(huán)里面還是循環(huán)外面?

所以這就是鎖粗化關注的點了,當JIT發(fā)現(xiàn)一系列連續(xù)的操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作出現(xiàn)在循環(huán)體中的時候,會將加鎖同步的范圍擴散(粗化)到整個操作序列的外部。
輕量級鎖
輕量級鎖是JDK 1.6之中加入的新型鎖機制,它名字中的“輕量級”是相對于使用操作系統(tǒng)互斥量來實現(xiàn)的傳統(tǒng)鎖而言的,因此傳統(tǒng)的鎖機制就稱為“重量級”鎖。 首先需要強調(diào)一點的是,輕量級鎖并不是用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。
在代碼進入同步塊的時候,如果此同步對象沒有被鎖定(鎖標志位為“01”狀態(tài)),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced前綴,即Displaced Mark Word),這時候線程堆棧與對象頭的狀態(tài)如下圖所示:

然后,虛擬機將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針。如果這個更新動作成功了,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標志位(Mark Word的最后2bit)將轉(zhuǎn)變?yōu)椤?0”,即表示此對象處于輕量級鎖定狀態(tài),這時候線程堆棧與對象頭的狀態(tài)如下圖所示:

如果這個更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果只說明當前線程已經(jīng)擁有了這個對象的鎖,那就可以直接進入同步塊繼續(xù)執(zhí)行,否則說明這個鎖對象已經(jīng)被其他線程搶占了。 如果有兩條以上的線程爭用同一個鎖,那輕量級鎖就不再有效,要膨脹為重量級鎖,鎖標志的狀態(tài)值變?yōu)椤?0”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進入阻塞狀態(tài)。
上面描述的是輕量級鎖的加鎖過程,它的解鎖過程也是通過CAS操作來進行的,如果對象的Mark Word仍然指向著線程的鎖記錄,那就用CAS操作把對象當前的Mark Word和線程中復制的Displaced Mark Word替換回來,如果替換成功,整個同步過程就完成了。 如果替換失敗,說明有其他線程嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的線程。
輕量級鎖能提升程序同步性能的依據(jù)是“對于絕大部分的鎖,在整個同步周期內(nèi)都是不存在競爭的”,這是一個經(jīng)驗數(shù)據(jù)。 如果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發(fā)生了CAS操作,因此在有競爭的情況下,輕量級鎖會比傳統(tǒng)的重量級鎖更慢。
偏向鎖
偏向鎖也是JDK 1.6中引入的一項鎖優(yōu)化,它的目的是消除數(shù)據(jù)在無競爭情況下的同步原語,進一步提高程序的運行性能。 如果說輕量級鎖是在無競爭的情況下使用CAS操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步都消除掉,連CAS操作都不做了。
偏向鎖的“偏”,就是偏心的“偏”、 偏袒的“偏”,它的意思是這個鎖會偏向于第一個獲得它的線程,如果在接下來的執(zhí)行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步。
如果讀懂了前面輕量級鎖中關于對象頭Mark Word與線程之間的操作過程,那偏向鎖的原理理解起來就會很簡單。 假設當前虛擬機啟用了偏向鎖(啟用參數(shù)-XX:+UseBiasedLocking,這是JDK 1.6的默認值),那么,當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標志位設為“01”,即偏向模式。 同時使用CAS操作把獲取到這個鎖的線程的ID記錄在對象的Mark Word之中,如果CAS操作成功,持有偏向鎖的線程以后每次進入這個鎖相關的同步塊時,虛擬機都可以不再進行任何同步操作(例如Locking、 Unlocking及對Mark Word的Update等)。
當有另外一個線程去嘗試獲取這個鎖時,偏向模式就宣告結(jié)束。 根據(jù)鎖對象目前是否處于被鎖定的狀態(tài),撤銷偏向(Revoke Bias)后恢復到未鎖定(標志位為“01”)或輕量級鎖定(標志位為“00”)的狀態(tài),后續(xù)的同步操作就如上面介紹的輕量級鎖那樣執(zhí)行。 偏向鎖、 輕量級鎖的狀態(tài)轉(zhuǎn)化及對象Mark Word的關系如下圖所示:

偏向鎖可以提高帶有同步但無競爭的程序性能。 它同樣是一個帶有效益權衡(Trade Off)性質(zhì)的優(yōu)化,也就是說,它并不一定總是對程序運行有利,如果程序中大多數(shù)的鎖總是被多個不同的線程訪問,那偏向模式就是多余的。 在具體問題具體分析的前提下,有時候使用參數(shù)-XX:-UseBiasedLocking來禁止偏向鎖優(yōu)化反而可以提升性能。
逃逸分析
棧上分配
顧名思義,當虛擬機確定一個對象不會逃逸出方法之外,那么讓對象在棧上分配內(nèi)存,對象所占用的內(nèi)存空間就可以隨著棧幀的出棧而銷毀
我們先來看一段代碼的測試結(jié)果

我們使用上述jvm參數(shù)(開啟逃逸分析,關閉TLAB優(yōu)化,分配1g堆內(nèi)存,打開gc日志打?。?執(zhí)行以上代碼
再來調(diào)整一下jvm參數(shù)(僅關閉逃逸分析),重新執(zhí)行一次

可以看到,逃逸分析在判定局部變量testObject不會逃逸出當前方法作用域以后,會進行棧上分配優(yōu)化,但是由于我jdk使用的是混合模式,所以還是有gc日志打印。

在Hotspot中采用的是解釋器和編譯器并行的架構,所謂的混合模式就是解釋器和編譯器搭配使用,當程序啟動初期,采用解釋器執(zhí)行(同時會記錄相關的數(shù)據(jù),比如函數(shù)的調(diào)用次數(shù),循環(huán)語句執(zhí)行次數(shù)),節(jié)省編譯的時間。在使用解釋器執(zhí)行期間,記錄的函數(shù)運行的數(shù)據(jù),通過這些數(shù)據(jù)發(fā)現(xiàn)某些代碼是熱點代碼,采用編譯器對熱點代碼進行編譯,以及優(yōu)化(逃逸分析就是其中一種優(yōu)化技術)。
標量替換
標量(Scalar)是指一個無法再分解成更小的數(shù)據(jù)的數(shù)據(jù)。Java中的原始數(shù)據(jù)類型就是標量。相對的,那些還可以分解的數(shù)據(jù)叫做聚合量(Aggregate),Java中的對象就是聚合量,因為他可以分解成其他聚合量和標量。
在JIT階段,如果經(jīng)過逃逸分析,發(fā)現(xiàn)一個對象不會被外界訪問的話,那么經(jīng)過JIT優(yōu)化,就會把這個對象拆解成若干個其中包含的若干個成員變量來代替。這個過程就是標量替換。
還是上面的例子,我們再調(diào)整一下jvm參數(shù)(開啟逃逸分析、關閉TLAB、關閉標量替換)

上面的例子說明,棧上分配是通過標量替換實現(xiàn)的。
鎖消除
同虛擬機鎖優(yōu)化中的鎖消除。
總結(jié)
Java虛擬機屏蔽了與具體操作系統(tǒng)平臺相關的信息之外,還為程序員做了很多優(yōu)化,除了本文提到的優(yōu)化之外,還有公共子表達式消除、數(shù)組邊界檢查消除、方法內(nèi)聯(lián)、內(nèi)存及代碼位置變換優(yōu)化、TLAB、PLAB等優(yōu)化技術,讀者有興趣可深入研究
參考:
<深入理解java虛擬機>
<Java虛擬機規(guī)范第2版>
<Hotspot實戰(zhàn)>
https://www.hollischuang.com/archives/tag/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E5%A4%9A%E7%BA%BF%E7%A8%8B
http://www.itdecent.cn/p/04fcd0ea5af7
https://blog.csdn.net/hollis_chuang/article/details/80922794