您好,登錄后才能下訂單哦!
自Java 6/Java 7開始,Java虛擬機對內(nèi)部鎖的實現(xiàn)進行了一些優(yōu)化。這些優(yōu)化主要包括鎖消除(Lock Elision)、鎖粗化(Lock Coarsening)、偏向鎖(Biased Locking)以及適應(yīng)性鎖(Adaptive Locking)。這些優(yōu)化僅在Java虛擬機server模式下起作用(即運行Java程序時我們可能需要在命令行中指定Java虛擬機參數(shù)“-server”以開啟這些優(yōu)化)。
1 鎖消除
鎖消除(Lock Elision)是JIT編譯器對內(nèi)部鎖的具體實現(xiàn)所做的一種優(yōu)化。
鎖消除(Lock Elision)示意圖
在動態(tài)編譯同步塊的時候,JIT編譯器可以借助一種被稱為逃逸分析(Escape Analysis)的技術(shù)來判斷同步塊所使用的鎖對象是否只能夠被一個線程訪問而沒有被發(fā)布到其他線程。如果同步塊所使用的鎖對象通過這種分析被證實只能夠被一個線程訪問,那么JIT編譯器在編譯這個同步塊的時候并不生成synchronized所表示的鎖的申請與釋放對應(yīng)的機器碼,而僅生成原臨界區(qū)代碼對應(yīng)的機器碼,這就造成了被動態(tài)編譯的字節(jié)碼就像是不包含monitorenter(申請鎖)和monitorexit(釋放鎖)這兩個字節(jié)碼指令一樣,即消除了鎖的使用。這種編譯器優(yōu)化就被稱為鎖消除(Lock Elision),它使得特定情況下我們可以完全消除鎖的開銷。
Java標準庫中的有些類(比如StringBuffer)雖然是線程安全的,但是在實際使用中我們往往不在多個線程間共享這些類的實例。而這些類在實現(xiàn)線程安全的時候往往借助于內(nèi)部鎖。因此,這些類是鎖消除優(yōu)化的常見目標。
清單12-1 可進行鎖消除優(yōu)化的示例代碼
public class LockElisionExample { public static String toJSON(ProductInfo productInfo) { StringBuffer sbf = new StringBuffer(); sbf.append("{\"productID\":\"").append(productInfo.productID); sbf.append("\",\"categoryID\":\"").append(productInfo.categoryID); sbf.append("\",\"rank\":").append(productInfo.rank); sbf.append(",\"inventory\":").append(productInfo.inventory); sbf.append('}'); return sbf.toString(); } }
在上面例子中,JIT編譯器在編譯toJSON方法的時候會將其調(diào)用的StringBuffer.append/toString方法內(nèi)聯(lián)(Inline)到該方法之中,這相當于把StringBuffer.append/toString方法的方法體中的指令復(fù)制到toJSON方法體之中。這里的StringBuffer實例sbf是一個局部變量,并且該變量所引用的對象并沒有被發(fā)布到其他線程,因此sbf引用的對象只能夠被sbf所在的方法(toJSON方法)的當前執(zhí)行線程(一個線程)訪問。所以,JIT編譯器此時可以消除toJSON方法中從StringBuffer.append/toString方法的方法體復(fù)制的指令所使用的內(nèi)部鎖。在這個例子中,StringBuffer.append/toString方法本身所使用的鎖并不會被消除,因為系統(tǒng)中可能還有其他地方在使用StringBuffer,而這些代碼可能會共享StringBuffer實例。
鎖消除優(yōu)化所依賴的逃逸分析技術(shù)自Java SE 6u23起默認是開啟的,但是鎖消除優(yōu)化是在Java 7開始引入的。
從上述例子可以看出,鎖消除優(yōu)化還可能需要以JIT編譯器的內(nèi)聯(lián)優(yōu)化為前提。而一個方法是否會被JIT編譯器內(nèi)聯(lián)取決于該方法的熱度以及該方法對應(yīng)的字節(jié)碼的尺寸(Bytecode Size)。因此,鎖消除優(yōu)化能否被實施還取決于被調(diào)用的同步方法(或者帶同步塊的方法)是否能夠被內(nèi)聯(lián)。
鎖消除優(yōu)化告訴我們在該使用鎖的情況下必須使用鎖,而不必過多在意鎖的開銷。開發(fā)人員應(yīng)該在代碼的邏輯層面考慮是否需要加鎖,而至于代碼運行層面上某個鎖是否真的有必要使用則由JIT編譯器來決定。鎖消除優(yōu)化并不表示開發(fā)人員在編寫代碼的時候可以隨意使用內(nèi)部鎖(在不需要加鎖的情況下加鎖),因為鎖消除是JIT編譯器而不是javac所做的一種優(yōu)化,而一段代碼只有在其被執(zhí)行的頻率足夠大的情況下才有可能會被JIT編譯器優(yōu)化。也就是說在JIT編譯器優(yōu)化介入之前,只要源代碼中使用了內(nèi)部鎖,那么這個鎖的開銷就會存在。另外,JIT編譯器所執(zhí)行的內(nèi)聯(lián)優(yōu)化、逃逸分析以及鎖消除優(yōu)化本身都是有其開銷的。
在鎖消除的作用下,利用ThreadLocal將一個線程安全的對象(比如Random)作為一個線程特有對象來使用,不僅僅可以避免鎖的爭用,還可以徹底消除這些對象內(nèi)部所使用的鎖的開銷。
2 鎖粗化
鎖粗化(Lock Coarsening/Lock Merging)是JIT編譯器對內(nèi)部鎖的具體實現(xiàn)所做的一種優(yōu)化。
鎖粗化(Lock Coarsening)示意圖
對于相鄰的幾個同步塊,如果這些同步塊使用的是同一個鎖實例,那么JIT編譯器會將這些同步塊合并為一個大同步塊,從而避免了一個線程反復(fù)申請、釋放同一個鎖所導(dǎo)致的開銷。然而,鎖粗化可能導(dǎo)致一個線程持續(xù)持有一個鎖的時間變長,從而使得同步在該鎖之上的其他線程在申請鎖時的等待時間變長。例如上圖中,第1個同步塊結(jié)束和第2個同步塊開始之間的時間間隙中,其他線程本來是有機會獲得monitorX的,但是經(jīng)過鎖粗化之后由于臨界區(qū)的長度變長,這些線程在申請monitorX時所需的等待時間也相應(yīng)變長了。因此,鎖粗化不會被應(yīng)用到循環(huán)體內(nèi)的相鄰?fù)綁K。
相鄰的兩個同步塊之間如果存在其他語句,也不一定就會阻礙JIT編譯器執(zhí)行鎖粗化優(yōu)化,這是因為JIT編譯器可能在執(zhí)行鎖粗化優(yōu)化前將這些語句挪到(即指令重排序)后一個同步塊的臨界區(qū)之中(當然,JIT編譯器并不會將臨界區(qū)內(nèi)的代碼挪到臨界區(qū)之外)。
實際上,我們寫的代碼中可能很少會出現(xiàn)上圖中那種連續(xù)的同步塊。這種同一個鎖實例引導(dǎo)的相鄰?fù)綁K往往是JIT編譯器編譯之后形成的。
例如,在下面的例子中
清單12-2 可進行鎖粗化優(yōu)化的示例代碼
public class LockCoarseningExample { private final Random rnd = new Random(); public void simulate() { int iq1 = randomIQ(); int iq2 = randomIQ(); int iq3 = randomIQ(); act(iq1, iq2, iq3); } private void act(int... n) { // ... } // 返回隨機的智商值 public int randomIQ() { // 人類智商的標準差是15,平均值是100 return (int) Math.round(rnd.nextGaussian() * 15 + 100); } // ... }
simulate方法連續(xù)調(diào)用randomIQ方法來生成3個符合正態(tài)分布(高斯分布)的隨機智商(IQ)。在simulate方法被執(zhí)行得足夠頻繁的情況下,JIT編譯器可能對該方法執(zhí)行一系優(yōu)化:首先,JIT編譯器可能將randomIQ方法內(nèi)聯(lián)(inline)到simulate方法中,這相當于把randomIQ方法體中的指令復(fù)制到simulate方法之中。在此基礎(chǔ)上,randomIQ方法中的rnd.nextGaussian()調(diào)用也可能被內(nèi)聯(lián),這相當于把Random.nextGaussian()方法體中的指令復(fù)制到simulate方法之中。Random.nextGaussian()是一個同步方法,由于Random實例rnd可能被多個線程共享(因為simulate方法可能被多個線程執(zhí)行),因此JIT編譯器無法對Random.nextGaussian()方法本身執(zhí)行鎖消除優(yōu)化,這使得被內(nèi)聯(lián)到simulate方法中的Random.nextGaussian()方法體相當于一個由rnd引導(dǎo)的同步塊。經(jīng)過上述優(yōu)化之后,JIT編譯器便會發(fā)現(xiàn)simulate方法中存在3個相鄰的由rnd(Random實例)引導(dǎo)的同步塊,于是鎖粗化優(yōu)化便“粉墨登場”了。
鎖粗化默認是開啟的。如果要關(guān)閉這個特性,我們可以在Java程序的啟動命令行中添加虛擬機參數(shù)“-XX:-EliminateLocks”(開啟則可以使用虛擬機參數(shù)“-XX:+EliminateLocks”)。
3 偏向鎖
偏向鎖(Biased Locking)是Java虛擬機對鎖的實現(xiàn)所做的一種優(yōu)化。這種優(yōu)化基于這樣的觀測結(jié)果(Observation):大多數(shù)鎖并沒有被爭用(Contented),并且這些鎖在其整個生命周期內(nèi)至多只會被一個線程持有。然而,Java虛擬機在實現(xiàn)monitorenter字節(jié)碼(申請鎖)和monitorexit字節(jié)碼(釋放鎖)時需要借助一個原子操作(CAS操作),這個操作代價相對來說比較昂貴。因此,Java虛擬機會為每個對象維護一個偏好(Bias),即一個對象對應(yīng)的內(nèi)部鎖第1次被一個線程獲得,那么這個線程就會被記錄為該對象的偏好線程(Biased Thread)。這個線程后續(xù)無論是再次申請該鎖還是釋放該鎖,都無須借助原先(指未實施偏向鎖優(yōu)化前)昂貴的原子操作,從而減少了鎖的申請與釋放的開銷。
然而,一個鎖沒有被爭用并不代表僅僅只有一個線程訪問該鎖,當一個對象的偏好線程以外的其他線程申請該對象的內(nèi)部鎖時,Java虛擬機需要收回(Revoke)該對象對原偏好線程的“偏好”并重新設(shè)置該對象的偏好線程。這個偏好收回和重新分配過程的代價也是比較昂貴的,因此如果程序運行過程中存在比較多的鎖爭用的情況,那么這種偏好收回和重新分配的代價便會被放大。有鑒于此,偏向鎖優(yōu)化只適合于存在相當大一部分鎖并沒有被爭用的系統(tǒng)之中。如果系統(tǒng)中存在大量被爭用的鎖而沒有被爭用的鎖僅占極小的部分,那么我們可以考慮關(guān)閉偏向鎖優(yōu)化。
偏向鎖優(yōu)化默認是開啟的。要關(guān)閉偏向鎖優(yōu)化,我們可以在Java程序的啟動命令行中添加虛擬機參數(shù)“-XX:-UseBiasedLocking”(開啟偏向鎖優(yōu)化可以使用虛擬機參數(shù)“-XX:+UseBiasedLocking”)。
4 適應(yīng)性鎖
適應(yīng)性鎖(Adaptive Locking,也被稱為 Adaptive Spinning )是JIT編譯器對內(nèi)部鎖實現(xiàn)所做的一種優(yōu)化。
存在鎖爭用的情況下,一個線程申請一個鎖的時候如果這個鎖恰好被其他線程持有,那么這個線程就需要等待該鎖被其持有線程釋放。實現(xiàn)這種等待的一種保守方法——將這個線程暫停(線程的生命周期狀態(tài)變?yōu)榉荝unnable狀態(tài))。由于暫停線程會導(dǎo)致上下文切換,因此對于一個具體鎖實例來說,這種實現(xiàn)策略比較適合于系統(tǒng)中絕大多數(shù)線程對該鎖的持有時間較長的場景,這樣才能夠抵消上下文切換的開銷。另外一種實現(xiàn)方法就是采用忙等(Busy Wait)。所謂忙等相當于如下代碼所示的一個循環(huán)體為空的循環(huán)語句:
// 當鎖被其他線程持有時一直循環(huán) while (lockIsHeldByOtherThread){}
可見,忙等是通過反復(fù)執(zhí)行空操作(什么也不做)直到所需的條件成立為止而實現(xiàn)等待的。這種策略的好處是不會導(dǎo)致上下文切換,缺點是比較耗費處理器資源——如果所需的條件在相當長時間內(nèi)未能成立,那么忙等的循環(huán)就會一直被執(zhí)行。因此,對于一個具體的鎖實例來說,忙等策略比較適合于絕大多數(shù)線程對該鎖的持有時間較短的場景,這樣能夠避免過多的處理器時間開銷。
事實上,Java虛擬機也不是非要在上述兩種實現(xiàn)策略之中擇其一 ——它可以綜合使用上述兩種策略。對于一個具體的鎖實例,Java虛擬機會根據(jù)其運行過程中收集到的信息來判斷這個鎖是屬于被線程持有時間“較長”的還是“較短”的。對于被線程持有時間“較長”的鎖,Java虛擬機會選用暫停等待策略;而對于被線程持有時間“較短”的鎖,Java虛擬機會選用忙等等待策略。Java虛擬機也可能先采用忙等等待策略,在忙等失敗的情況下再采用暫停等待策略。Java虛擬機的這種優(yōu)化就被稱為適應(yīng)性鎖(Adaptive Locking),這種優(yōu)化同樣也需要JIT編譯器介入。
適應(yīng)性鎖優(yōu)化可以是以具體的一個鎖實例為基礎(chǔ)的。也就是說,Java虛擬機可能對一個鎖實例采用忙等等待策略,而對另外一個鎖實例采用暫停等待策略。
從適應(yīng)性鎖優(yōu)化可以看出,內(nèi)部鎖的使用并不一定會導(dǎo)致上下文切換,這就是我們說鎖與上下文切換時均說鎖“可能”導(dǎo)致上下文切換的原因。
本文選自《Java多線程編程實戰(zhàn)指南(核心篇)》。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持億速云?! ?/p>
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。