Java中synchronized的優(yōu)化措施
本文介紹為了實(shí)現(xiàn)高效并發(fā),虛擬機(jī)對(duì) synchronized 做的一系列的鎖優(yōu)化措施
高效并發(fā)是從 JDK5 升級(jí)到 JDK6 后一項(xiàng)重要的改進(jìn)項(xiàng),HotSpot 虛擬機(jī)開(kāi)發(fā)團(tuán)隊(duì)在 JDK6 這個(gè)版本上花費(fèi)了大量的資源去實(shí)現(xiàn)各種鎖優(yōu)化技術(shù),如適應(yīng)性自旋(Adaptive Spinning)、鎖消除(Lock Elimination)、鎖膨脹(Lock Coarsening)、 輕量級(jí)鎖(Lightweight Locking) 、偏向鎖(Biased Locking)等,這些技術(shù)都是為了在線程之間更高效地共享數(shù)據(jù)及解決競(jìng)爭(zhēng)問(wèn)題,從而提高程序的執(zhí)行效率。
自旋鎖 & 自適應(yīng)自旋
在許多應(yīng)用上,共享數(shù)據(jù)的鎖定狀態(tài)只會(huì)持續(xù)很短的一段時(shí)間,為了這段時(shí)間去掛起和恢復(fù)線程并不值得。
自旋鎖指的是:線程 A 成功獲取鎖后,線程 B 請(qǐng)求鎖時(shí),請(qǐng)求鎖的線程 B 執(zhí)行一個(gè)忙循環(huán)(自旋),不放棄處理器的執(zhí)行時(shí)間,看看持有鎖的線程 A 是否會(huì)很快就釋放鎖。自旋等待的時(shí)間有一定的限度,如果自旋超過(guò)了限定的次數(shù)仍然沒(méi)有成功獲得鎖,就應(yīng)當(dāng)使用傳統(tǒng)的方式去掛起線程。
自適應(yīng)自旋指的是:自旋的時(shí)間不再是固定的了,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定的。
前面我們討論互斥同步的時(shí)候,提到了互斥同步對(duì)性能影響最大的是阻塞的實(shí)現(xiàn),掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成,這些操作給Java虛擬機(jī)的并發(fā)性能帶來(lái)了很大的壓力。同時(shí),虛擬機(jī)的開(kāi)發(fā)團(tuán)隊(duì)也注意到在許多應(yīng)用上,共享數(shù)據(jù)的鎖定狀態(tài)只會(huì)持續(xù)很短的一段時(shí)間,為了這段時(shí)間去掛起和恢復(fù)線程并不值得?,F(xiàn)在絕大多數(shù)的個(gè)人電腦和服務(wù)器都是多路(核)處理器系統(tǒng),如果物理機(jī)器有一個(gè)以上的處理器或者處理器核心,能讓兩個(gè)或以上的線程同時(shí)并行執(zhí)行,我們就可以讓后面請(qǐng)求鎖的那個(gè)線程“稍等一會(huì)”,但不放棄處理器的執(zhí)行時(shí)間,看看持有鎖的線程是否很快就會(huì)釋放鎖。為了讓線程等待,我們只須讓線程執(zhí)行一個(gè)忙循環(huán)(自旋),這項(xiàng)技術(shù)就是所謂的自旋鎖。
自旋鎖在 JDK1.4.2 中就已經(jīng)引入,只不過(guò)默認(rèn)是關(guān)閉的,可以使用 -XX:+UseSpinning 參數(shù)來(lái)開(kāi)啟,在 JDK6 中就已經(jīng)改為默認(rèn)開(kāi)啟了。自旋等待不能代替阻塞,且先不說(shuō)對(duì)處理器數(shù)量的要求,自旋等待本身雖然避免了線程切換的開(kāi)銷,但它是要占用處理器時(shí)間的,所以如果鎖被占用的時(shí)間很短,自旋等待的效果就會(huì)非常好,反之如果鎖被占用的時(shí)間很長(zhǎng), 那么自旋的線程只會(huì)白白消耗處理器資源,而不會(huì)做任何有價(jià)值的工作,這就會(huì)帶來(lái)性能的浪費(fèi)。因此自旋等待的時(shí)間必須有一定的限度,如果自旋超過(guò)了限定的次數(shù)仍然沒(méi)有成功獲得鎖,就應(yīng)當(dāng)使用傳統(tǒng)的方式去掛起線程。自旋次數(shù)的默認(rèn)值是十次,用戶也可以使用參數(shù) -XX:PreBlockSpin 來(lái)自行更改。
不過(guò)無(wú)論是默認(rèn)值還是用戶指定的自旋次數(shù),對(duì)整個(gè) Java 虛擬機(jī)中所有的鎖來(lái)說(shuō)都是相同的。在 JDK6 中對(duì)自旋鎖的優(yōu)化,引入了自適應(yīng)的自旋。自適應(yīng)意味著自旋的時(shí)間不再是固定的了,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定的。
- 如果在同一個(gè)鎖對(duì)象上,自旋等待剛剛成功獲得過(guò)鎖, 并且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也很有可能再次成功,進(jìn)而允許自旋等待持續(xù)相對(duì)更長(zhǎng)的時(shí)間,比如持續(xù)100次忙循環(huán)。
- 另一方面,如果對(duì)于某個(gè)鎖,自旋很少成功獲得過(guò)鎖,那在以后要獲取這個(gè)鎖時(shí)將有可能直接省略掉自旋過(guò)程,以避免浪費(fèi)處理器資源。
有了自適應(yīng)自旋,隨著程序運(yùn)行時(shí)間的增長(zhǎng)及性能監(jiān)控信息的不斷完善,虛擬機(jī)對(duì)程序鎖的狀況預(yù)測(cè)就會(huì)越來(lái)越精準(zhǔn),虛擬機(jī)就會(huì)變得越來(lái)越“聰明”了。
鎖消除
鎖消除是指虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí),對(duì)一些代碼要求同步,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除。鎖消除的主要判定依據(jù)來(lái)源于逃逸分析的數(shù)據(jù)支持,如果判斷到一段代碼中,在堆上的所有數(shù)據(jù)都不會(huì)逃逸出去被其他線程訪問(wèn)到,那就可以把它們當(dāng)作棧上數(shù)據(jù)對(duì)待,認(rèn)為它們是線程私有的,同步加鎖自然就無(wú)須再進(jìn)行。
也許讀者會(huì)有疑問(wèn),變量是否逃逸,對(duì)于虛擬機(jī)來(lái)說(shuō)是需要使用復(fù)雜的過(guò)程間分析才能確定的,但是程序員自己應(yīng)該是很清楚的,怎么會(huì)在明知道不存在數(shù)據(jù)爭(zhēng)用的情況下還要求同步呢?這個(gè)問(wèn)題的答案是:有許多同步措施并不是程序員自己加入的,同步的代碼在 Java 程序中出現(xiàn)的頻繁程度也許超過(guò)了大部分讀者的想象。我們來(lái)看看如代碼清單13-6所示的例子,這段非常簡(jiǎn)單的代碼僅僅是輸出三個(gè)字符串相加的結(jié)果,無(wú)論是源代碼字面上, 還是程序語(yǔ)義上都沒(méi)有進(jìn)行同步。
// 代碼清單13-6 一段看起來(lái)沒(méi)有同步的代碼 public String concatString(String s1, String s2, String s3) { return s1 + s2 + s3; }
我們也知道,由于 String 是一個(gè)不可變的類,對(duì)字符串的連接操作總是通過(guò)生成新的 String 對(duì)象來(lái)進(jìn)行的,因此 Javac 編譯器會(huì)對(duì) String 連接做自動(dòng)優(yōu)化。
- 在 JDK5 之前,字符串加法會(huì)轉(zhuǎn)化為 StringBuffer 對(duì)象的連續(xù) append() 操作。即代碼清單13-6所示的代碼可能會(huì)變成代碼清單13-7所示的樣子。
- 在 JDK5 及以后的版本中,會(huì)轉(zhuǎn)化為 StringBuilder 對(duì)象的連續(xù) append() 操作。
// 代碼清單13-7 Javac轉(zhuǎn)化后的字符串連接操作 public String concatString(String s1, String s2, String s3) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); sb.append(s3); return sb.toString(); }
現(xiàn)在大家還認(rèn)為這段代碼沒(méi)有涉及同步嗎?每個(gè) StringBuffer.append() 方法中都有一個(gè)同步塊,鎖就是 sb 對(duì)象。虛擬機(jī)觀察 sb 變量,經(jīng)過(guò)逃逸分析后會(huì)發(fā)現(xiàn)它的動(dòng)態(tài)作用域被限制在 concatString() 方法內(nèi)部。也就是 sb 的所有引用都永遠(yuǎn)不會(huì)逃逸到 concatString() 方法之外,其他線程無(wú)法訪問(wèn)到它,所以這里雖然有鎖,但是可以被安全地消除掉。在解釋執(zhí)行時(shí)這里仍然會(huì)加鎖,但在經(jīng)過(guò)服務(wù)端編譯器的即時(shí)編譯之后,這段代碼就會(huì)忽略所有的同步措施而直接執(zhí)行。
鎖粗化
鎖粗化指的是:如果虛擬機(jī)探測(cè)到有一串零碎的操作都對(duì)同一個(gè)對(duì)象加鎖,那么虛擬機(jī)將會(huì)把加鎖同步的范圍擴(kuò)展(粗化)到整個(gè)操作序列的外部。
原則上,我們?cè)诰帉?xiě)代碼的時(shí)候,總是推薦將同步塊的作用范圍限制得盡量?。褐辉诠蚕頂?shù)據(jù)的實(shí)際作用域中才進(jìn)行同步,這樣是為了使得需要同步的操作數(shù)量盡可能變少,即使存在鎖競(jìng)爭(zhēng),等待鎖的線程也能盡可能快地拿到鎖。
大多數(shù)情況下,上面的原則都是正確的,但是如果一系列的連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體之中的,那即使沒(méi)有線程競(jìng)爭(zhēng),頻繁地進(jìn)行互斥同步操作也會(huì)導(dǎo)致不必要的性能損耗。
代碼清單13-7所示連續(xù)的 append() 方法就屬于這類情況。如果虛擬機(jī)探測(cè)到有這樣一串零碎的操作都對(duì)同一個(gè)對(duì)象加鎖,將會(huì)把加鎖同步的范圍擴(kuò)展(粗化)到整個(gè)操作序列的外部,以代碼清單13-7為例,就是擴(kuò)展到第一個(gè) append() 操作之前直至最后一個(gè) append() 操作之后,這樣只需要加鎖一次就可以了。
輕量級(jí)鎖
輕量級(jí)鎖的設(shè)計(jì)初衷是在沒(méi)有多線程競(jìng)爭(zhēng)的情況下,通過(guò)使用 CAS(Compare And Swap)操作來(lái)進(jìn)行線程同步,減少傳統(tǒng)的重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。
輕量級(jí)鎖可以提高帶有同步但無(wú)競(jìng)爭(zhēng)的程序性能,但它是一個(gè)帶有效益權(quán)衡(Trade Off) 性質(zhì)的優(yōu)化,也就是說(shuō)它并非總是對(duì)程序運(yùn)行有利。輕量級(jí)鎖能提升程序同步性能的依據(jù)是 “對(duì)于絕大部分的鎖,在整個(gè)同步周期內(nèi)都是不存在競(jìng)爭(zhēng)的” 這一經(jīng)驗(yàn)法則。
- 如果沒(méi)有競(jìng)爭(zhēng),輕量級(jí)鎖便通過(guò) CAS 操作成功避免了使用互斥量的開(kāi)銷;
- 但如果確實(shí)存在鎖競(jìng)爭(zhēng),除了互斥量的本身開(kāi)銷外,還額外發(fā)生了 CAS 操作的開(kāi)銷。
因此在有競(jìng)爭(zhēng)的情況下,輕量級(jí)鎖反而會(huì)比傳統(tǒng)的重量級(jí)鎖更慢。
輕量級(jí)鎖是 JDK6 時(shí)加入的新型鎖機(jī)制,它名字中的 “輕量級(jí)” 是相對(duì)于使用操作系統(tǒng)互斥量來(lái)實(shí)現(xiàn)的傳統(tǒng)鎖而言的, 因此傳統(tǒng)的鎖機(jī)制就被稱為“重量級(jí)”鎖。不過(guò),需要強(qiáng)調(diào)一點(diǎn),輕量級(jí)鎖并不是用來(lái)代替重量級(jí)鎖的,輕量級(jí)鎖設(shè)計(jì)的初衷是在沒(méi)有多線程競(jìng)爭(zhēng)的前提下,減少傳統(tǒng)的重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。
Mark Word
要理解輕量級(jí)鎖,以及后面會(huì)講到的偏向鎖的原理和運(yùn)作過(guò)程,必須要對(duì) HotSpot 虛擬機(jī)對(duì)象的內(nèi)存布局(尤其是對(duì)象頭部分)有所了解。HotSpot 虛擬機(jī)的對(duì)象頭(Object Header)分為兩部分:
- 第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC 分代年齡(Generational GC Age)等。這部分?jǐn)?shù)據(jù)的長(zhǎng)度在 32 位和 64 位的 Java 虛擬機(jī)中分別會(huì)占用 32 個(gè)或 64 個(gè)比特,官方稱它為 “Mark Word”。這部分是實(shí)現(xiàn)輕量級(jí)鎖和偏向鎖的關(guān)鍵。
- 另外一部分用于存儲(chǔ)指向方法區(qū)對(duì)象類型數(shù)據(jù)的指針(Class Pointer、類型指針),虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。如果是數(shù)組對(duì)象,還會(huì)有一個(gè)額外的部分用于存儲(chǔ)數(shù)組長(zhǎng)度。
由于對(duì)象頭信息是與對(duì)象自身定義的數(shù)據(jù)無(wú)關(guān)的額外存儲(chǔ)成本,考慮到 Java 虛擬機(jī)的空間使用效率,Mark Word 被設(shè)計(jì)成一個(gè)非固定的動(dòng)態(tài)數(shù)據(jù)結(jié)構(gòu),以便在極小的空間內(nèi)存儲(chǔ)盡量多的信息。它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間。例如在 32 位的 HotSpot 虛擬機(jī)中:
- 對(duì)象未被鎖定的狀態(tài)下,Mark Word 的 32 個(gè)比特空間里的 25 個(gè)比特將用于存儲(chǔ)對(duì)象哈希碼,4 個(gè)比特用于存儲(chǔ)對(duì)象分代年齡,2 個(gè)比特用于存儲(chǔ)鎖標(biāo)志位,還有 1 個(gè)比特固定為 0(這表示未進(jìn)入偏向模式)。
- 對(duì)象除了未被鎖定的正常狀態(tài)外,還有輕量級(jí)鎖定、重量級(jí)鎖定、GC 標(biāo)記、可偏向等幾種不同狀態(tài),這些狀態(tài)下對(duì)象頭的存儲(chǔ)內(nèi)容如下表所示。
工作過(guò)程
我們簡(jiǎn)單回顧了對(duì)象的內(nèi)存布局后,接下來(lái)就可以介紹輕量級(jí)鎖的工作過(guò)程了:在代碼即將進(jìn)入同步塊的時(shí)候,如果此同步對(duì)象沒(méi)有被鎖定(鎖標(biāo)志位為“01”狀態(tài)),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間, 用于存儲(chǔ)鎖對(duì)象目前的 Mark Word 的拷貝(官方為這份拷貝加了一個(gè) Displaced 前綴,即 Displaced Mark Word),這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如圖13-3所示。
圖13-3輕量級(jí)鎖 CAS 操作之前堆棧與對(duì)象的狀態(tài)
然后, 虛擬機(jī)將使用 CAS 操作嘗試把對(duì)象的 Mark Word 更新為指向鎖記錄(Lock Record)的指針。
- 如果這個(gè)更新操作成功了,即代表該線程擁有了這個(gè)對(duì)象的鎖,并且對(duì)象 Mark Word 的鎖標(biāo)志位(Mark Word 的最后兩個(gè)比特)將轉(zhuǎn)變?yōu)?“00”,表示此對(duì)象處于輕量級(jí)鎖定狀態(tài)。這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如圖13-4所示。
- 如果這個(gè)更新操作失敗了,那就意味著至少存在一條線程與當(dāng)前線程競(jìng)爭(zhēng)獲取該對(duì)象的鎖。虛擬機(jī)首先會(huì)檢查對(duì)象的 Mark Word 是否指向當(dāng)前線程的棧幀,如果是,說(shuō)明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,那直接進(jìn)入同步塊繼續(xù)執(zhí)行就可以了,否則(對(duì)象的 Mark Word 不是指向當(dāng)前線程的棧幀)就說(shuō)明這個(gè)鎖對(duì)象已經(jīng)被其他線程搶占了。如果出現(xiàn)兩條以上的線程爭(zhēng)用同一個(gè)鎖的情況,那輕量級(jí)鎖就不再有效,必須要膨脹為重量級(jí)鎖,鎖標(biāo)志的狀態(tài)值變?yōu)?ldquo;10”,此時(shí) Mark Word 中存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針,后面等待鎖的線程也必須進(jìn)入阻塞狀態(tài)。
圖13-4輕量級(jí)鎖 CAS 操作之后堆棧與對(duì)象的狀態(tài)
上面描述的是輕量級(jí)鎖的加鎖過(guò)程,它的解鎖過(guò)程也同樣是通過(guò) CAS 操作來(lái)進(jìn)行的,如果對(duì)象的 Mark Word 仍然指向線程的鎖記錄,那就用 CAS 操作把對(duì)象當(dāng)前的 Mark Word 和線程中復(fù)制的 Displaced Mark Word 替換回來(lái)。
- 假如能夠替換成功,那整個(gè)同步過(guò)程就順利完成了;
- 如果替換失敗,則說(shuō)明有其他線程嘗試過(guò)獲取該鎖,就要在釋放鎖的同時(shí),喚醒被掛起的線程。
輕量級(jí)鎖能提升程序同步性能的依據(jù)是 “對(duì)于絕大部分的鎖,在整個(gè)同步周期內(nèi)都是不存在競(jìng)爭(zhēng)的” 這一經(jīng)驗(yàn)法則。
- 如果沒(méi)有競(jìng)爭(zhēng),輕量級(jí)鎖便通過(guò) CAS 操作成功避免了使用互斥量的開(kāi)銷;
- 但如果確實(shí)存在鎖競(jìng)爭(zhēng),除了互斥量的本身開(kāi)銷外,還額外發(fā)生了 CAS 操作的開(kāi)銷。
因此在有競(jìng)爭(zhēng)的情況下,輕量級(jí)鎖反而會(huì)比傳統(tǒng)的重量級(jí)鎖更慢。
偏向鎖
偏向鎖的目的是:消除數(shù)據(jù)在無(wú)競(jìng)爭(zhēng)情況下的同步原語(yǔ),進(jìn)一步提高程序的運(yùn)行性能。
偏向鎖中的“偏”的意思是這個(gè)鎖會(huì)偏向于第一個(gè)獲得它的線程。如果虛擬機(jī)啟用了偏向鎖,那么當(dāng)鎖對(duì)象第一次被線程獲取的時(shí)候,虛擬機(jī)將會(huì)把對(duì)象頭中的標(biāo)志位設(shè)置為 “01”、把偏向模式設(shè)置為 “1”,表示進(jìn)入偏向模式。同時(shí)使用 CAS 操作把獲取到這個(gè)鎖的線程的 ID 記錄在對(duì)象的 Mark Word 之中。如果 CAS 操作成功,持有偏向鎖的線程以后每次進(jìn)入這個(gè)鎖相關(guān)的同步塊時(shí),虛擬機(jī)都可以不再進(jìn)行任何同步操作(例如加鎖、解鎖及對(duì) Mark Word 的更新操作等)。
偏向鎖可以提高帶有同步但無(wú)競(jìng)爭(zhēng)的程序性能,但它同樣是一個(gè)帶有效益權(quán)衡(Trade Off) 性質(zhì)的優(yōu)化,也就是說(shuō)它并非總是對(duì)程序運(yùn)行有利。如果程序中大多數(shù)的鎖都總是被多個(gè)不同的線程訪問(wèn),那偏向模式就是多余的。
偏向鎖也是 JDK6 中引入的一項(xiàng)鎖優(yōu)化措施,它的目的是消除數(shù)據(jù)在無(wú)競(jìng)爭(zhēng)情況下的同步原語(yǔ),進(jìn)一步提高程序的運(yùn)行性能。如果說(shuō)輕量級(jí)鎖是在無(wú)競(jìng)爭(zhēng)的情況下使用 CAS 操作去消除同步使用的互斥量,那偏向鎖就是在無(wú)競(jìng)爭(zhēng)的情況下把整個(gè)同步都消除掉,連 CAS 操作都不去做了。
偏向鎖中的“偏”,就是偏心的“偏”、偏袒的“偏”。偏向鎖中的“偏”的意思是這個(gè)鎖會(huì)偏向于第一個(gè)獲得它的線程,如果在接下來(lái)的執(zhí)行過(guò)程中,該鎖一直沒(méi)有被其他的線程獲取,則持有偏向鎖的線程將永遠(yuǎn)不需要再進(jìn)行同步。
如果讀者理解了前面輕量級(jí)鎖中關(guān)于對(duì)象頭 Mark Word 與線程之間的操作過(guò)程,那偏向鎖的原理就會(huì)很容易理解。
假設(shè)當(dāng)前虛擬機(jī)啟用了偏向鎖(啟用參數(shù) -XX:+UseBiased Locking,這是自 JDK6 起 HotSpot 虛擬機(jī)的默認(rèn)值),那么當(dāng)鎖對(duì)象第一次被線程獲取的時(shí)候,虛擬機(jī)將會(huì)把對(duì)象頭中的標(biāo)志位設(shè)置為 “01”、把偏向模式設(shè)置為 “1”,表示進(jìn)入偏向模式。同時(shí)使用 CAS 操作把獲取到這個(gè)鎖的線程的 ID 記錄在對(duì)象的 Mark Word 之中。如果 CAS 操作成功,持有偏向鎖的線程以后每次進(jìn)入這個(gè)鎖相關(guān)的同步塊時(shí),虛擬機(jī)都可以不再進(jìn)行任何同步操作(例如加鎖、解鎖及對(duì) Mark Word 的更新操作等)。
一旦出現(xiàn)另外一個(gè)線程去嘗試獲取這個(gè)鎖的情況,偏向模式就馬上宣告結(jié)束。根據(jù)鎖對(duì)象目前是否處于被鎖定的狀態(tài)決定是否撤銷偏向(偏向模式設(shè)置為 “0”),撤銷后標(biāo)志位恢復(fù)到未鎖定(標(biāo)志位為 “01”)或輕量級(jí)鎖定(標(biāo)志位為 “00”)的狀態(tài),后續(xù)的同步操作就按照上面介紹的輕量級(jí)鎖那樣去執(zhí)行。
偏向鎖、輕量級(jí)鎖的狀態(tài)轉(zhuǎn)換及對(duì)象 Mark Word 的關(guān)系如圖13-5所示。
圖13-5偏向鎖、輕量級(jí)鎖的狀態(tài)轉(zhuǎn)換及及對(duì)象 Mark Word 的關(guān)系
細(xì)心的讀者看到這里可能會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題:當(dāng)對(duì)象進(jìn)入偏向狀態(tài)的時(shí)候,Mark Word 大部分的空間(23個(gè)比特) 都用于存儲(chǔ)持有鎖的線程 ID 了,這部分空間占用了原有存儲(chǔ)對(duì)象哈希碼的位置,那原來(lái)對(duì)象的哈希碼怎么辦呢?
在 Java 語(yǔ)言里面一個(gè)對(duì)象如果計(jì)算過(guò)哈希碼,就應(yīng)該一直保持該值不變(強(qiáng)烈推薦但不強(qiáng)制,因?yàn)橛脩艨梢灾剌dhashCode() 方法按自己的意愿返回哈希碼),否則很多依賴對(duì)象哈希碼的 API 都可能存在出錯(cuò)風(fēng)險(xiǎn)。而作為絕大多數(shù)對(duì)象哈希碼來(lái)源的 Object::hashCode() 方法,返回的是對(duì)象的一致性哈希碼(Identity Hash Code),這個(gè)值是能強(qiáng)制保證不變的,它通過(guò)在對(duì)象頭中存儲(chǔ)計(jì)算結(jié)果來(lái)保證第一次計(jì)算之后,再次調(diào)用該方法取到的哈希碼值永遠(yuǎn)不會(huì)再發(fā)生改變。 因此,當(dāng)一個(gè)對(duì)象已經(jīng)計(jì)算過(guò)一致性哈希碼后,它就再也無(wú)法進(jìn)入偏向鎖狀態(tài)了;而當(dāng)一個(gè)對(duì)象當(dāng)前正處于偏向鎖狀態(tài), 又收到需要計(jì)算其一致性哈希碼請(qǐng)求時(shí),它的偏向狀態(tài)會(huì)被立即撤銷,并且鎖會(huì)膨脹為重量級(jí)鎖。在重量級(jí)鎖的實(shí)現(xiàn)中, 對(duì)象頭指向了重量級(jí)鎖的位置,代表重量級(jí)鎖的 ObjectMonitor 類里有字段可以記錄非加鎖狀態(tài)(標(biāo)志位為“01”)下的Mark Word,其中自然可以存儲(chǔ)原來(lái)的哈希碼。
注意, 這里說(shuō)的計(jì)算請(qǐng)求應(yīng)來(lái)自于對(duì)Object::hashCode()或者System::identityHashCode(Object)方法的調(diào)用, 如果重寫(xiě)了對(duì)象的hashCode()方法, 計(jì)算哈希碼時(shí)并不會(huì)產(chǎn)生這里所說(shuō)的請(qǐng)求。
偏向鎖可以提高帶有同步但無(wú)競(jìng)爭(zhēng)的程序性能,但它同樣是一個(gè)帶有效益權(quán)衡(Trade Off) 性質(zhì)的優(yōu)化,也就是說(shuō)它并非總是對(duì)程序運(yùn)行有利。如果程序中大多數(shù)的鎖都總是被多個(gè)不同的線程訪問(wèn),那偏向模式就是多余的。在具體問(wèn)題具體分析的前提下,有時(shí)候使用參數(shù)-XX:-UseBiasedLocking 來(lái)禁止偏向鎖優(yōu)化反而可以提升性能。
完整的過(guò)程
假設(shè)當(dāng)前虛擬機(jī)啟用了偏向鎖,那么當(dāng)鎖對(duì)象第一次被線程獲取的時(shí)候,虛擬機(jī)將會(huì)把對(duì)象頭中的標(biāo)志位設(shè)置為 “01”、把偏向模式設(shè)置為 “1”,表示進(jìn)入偏向模式。同時(shí)使用 CAS 操作把獲取到這個(gè)鎖的線程的 ID 記錄在對(duì)象的 Mark Word 之中。如果 CAS 操作成功,持有偏向鎖的線程以后每次進(jìn)入這個(gè)鎖相關(guān)的同步塊時(shí),虛擬機(jī)都可以不再進(jìn)行任何同步操作(例如加鎖、解鎖及對(duì) Mark Word 的更新操作等)。
如果鎖對(duì)象目前處于偏向模式,那么一旦出現(xiàn)另外一個(gè)線程去嘗試獲取這個(gè)鎖的情況,偏向模式就馬上宣告結(jié)束。根據(jù)鎖對(duì)象目前是否處于被鎖定的狀態(tài)決定撤銷偏向后,鎖對(duì)象處于什么狀態(tài)。
- 如果鎖對(duì)象目前處于被鎖定的狀態(tài),那么一旦出現(xiàn)另外一個(gè)線程去嘗試獲取這個(gè)鎖的情況,偏向模式就馬上宣告結(jié)束,鎖對(duì)象轉(zhuǎn)換到輕量級(jí)鎖定狀態(tài),后續(xù)的同步操作就按照輕量級(jí)鎖那樣去執(zhí)行。
- 如果鎖對(duì)象目前處于未被鎖定的狀態(tài),那么一旦出現(xiàn)另外一個(gè)線程去嘗試獲取這個(gè)鎖的情況,偏向模式就馬上宣告結(jié)束,鎖對(duì)象轉(zhuǎn)換到未被鎖定、不可偏向狀態(tài)。
對(duì)象轉(zhuǎn)換到輕量級(jí)鎖定狀態(tài)。虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的 Mark Word 的拷貝。然后,虛擬機(jī)將使用 CAS 操作嘗試把對(duì)象的 Mark Word 更新為指向鎖記錄(Lock Record)的指針。
- 如果這個(gè)更新操作成功了,即代表該線程擁有了這個(gè)對(duì)象的鎖,并且對(duì)象 Mark Word 的鎖標(biāo)志位將轉(zhuǎn)變?yōu)?“00”,表示此對(duì)象處于輕量級(jí)鎖定狀態(tài)。
- 如果這個(gè)更新操作失敗了,那就意味著至少存在一條線程與當(dāng)前線程競(jìng)爭(zhēng)獲取該對(duì)象的鎖。虛擬機(jī)首先會(huì)檢查對(duì)象的 Mark Word 是否指向當(dāng)前線程的棧幀:
- 如果是(對(duì)象的 Mark Word 指向當(dāng)前線程的棧幀),說(shuō)明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,那直接進(jìn)入同步塊繼續(xù)執(zhí)行就可以了;
- 否則(對(duì)象的 Mark Word 不是指向當(dāng)前線程的棧幀)就說(shuō)明這個(gè)鎖對(duì)象已經(jīng)被其他線程搶占了,那么當(dāng)前線程 B 執(zhí)行一個(gè)忙循環(huán)(自旋),不放棄處理器的執(zhí)行時(shí)間,看看持有鎖的線程 A 是否會(huì)很快就釋放鎖。
- 如果持有鎖的線程 A 很快就釋放了鎖,那么當(dāng)前線程 B 成功獲取鎖。
- 如果線程 B 自旋超過(guò)了限定的次數(shù)仍然沒(méi)有成功獲得鎖,那輕量級(jí)鎖就不再有效,必須要膨脹為重量級(jí)鎖,鎖標(biāo)志的狀態(tài)值變?yōu)?ldquo;10”,此時(shí) Mark Word 中存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針。當(dāng)前線程繼續(xù)等待鎖,并進(jìn)入阻塞狀態(tài)。持有鎖的線程 A 釋放鎖的同時(shí),喚醒被掛起的線程。被喚醒的線程就會(huì)進(jìn)行新一輪的競(jìng)爭(zhēng),嘗試獲取這個(gè)鎖。
參考資料
第13章 線程安全與鎖優(yōu)化 13.3 鎖優(yōu)化
到此這篇關(guān)于Java中synchronized的優(yōu)化 的文章就介紹到這了,更多相關(guān)Java synchronized優(yōu)化 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java 實(shí)現(xiàn)麥克風(fēng)自動(dòng)錄音
這篇文章主要介紹了Java 實(shí)現(xiàn)麥克風(fēng)自動(dòng)錄音的示例代碼,幫助大家更好的理解和使用Java,感興趣的朋友可以了解下2020-12-12mybatis不加@Parm注解報(bào)錯(cuò)的解決方案
這篇文章主要介紹了mybatis不加@Parm注解報(bào)錯(cuò)的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11IDEA Spring Boot 自動(dòng)化構(gòu)建+部署的實(shí)現(xiàn)
這篇文章主要介紹了IDEA Spring Boot 自動(dòng)化構(gòu)建+部署的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01Spring Boot基礎(chǔ)學(xué)習(xí)之Mybatis操作中使用Redis做緩存詳解
這篇文章主要給大家介紹了關(guān)于Spring Boot基礎(chǔ)學(xué)習(xí)之Mybatis操作中使用Redis做緩存的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用spring boot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧2018-11-11詳解Java接口簽名(Signature)實(shí)現(xiàn)方案
這篇文章主要介紹了Java接口簽名(Signature)實(shí)現(xiàn)方案?,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-01-01Java中繼承thread類與實(shí)現(xiàn)Runnable接口的比較
這篇文章主要介紹了Java中繼承thread類與實(shí)現(xiàn)Runnable接口的比較的相關(guān)資料,需要的朋友可以參考下2017-06-06Spring Boot Rest控制器單元測(cè)試過(guò)程解析
這篇文章主要介紹了Spring Boot Rest控制器單元測(cè)試過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03