面试官:你了解JVM的锁优化吗?
本文分享自百度開發(fā)者中心面試官:你了解JVM的鎖優(yōu)化嗎?
鎖優(yōu)化.md
文章已同步至 GitHub 開源項(xiàng)目: JVM 底層原理解析
?高效并發(fā)是 JDK5 升級(jí)到 JDK6 后一項(xiàng)重要的改進(jìn),HotSpot 虛擬機(jī)開發(fā)團(tuán)隊(duì)在這個(gè)版本上花費(fèi)了巨大的資源去實(shí)現(xiàn)各種鎖優(yōu)化。比如,自旋鎖,自適應(yīng)自旋鎖,鎖消除,鎖膨脹,輕量級(jí)鎖,偏向鎖等。這些技術(shù)都是為了在線程之間更高效的共享數(shù)據(jù)及解決競(jìng)爭(zhēng)問題。從而提高程序的運(yùn)行效率。
自旋鎖和自適應(yīng)自旋鎖
1.自旋鎖?
在互斥同步的時(shí)候,對(duì)性能影響最大的就是阻塞的實(shí)現(xiàn),掛起線程,恢復(fù)線程等的操作都需要用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài)去完成。這些操作給性能帶來了巨大的壓力。
?虛擬機(jī)的開發(fā)團(tuán)隊(duì)也注意到,共享數(shù)據(jù)的鎖定狀態(tài)只會(huì)持續(xù)很短的時(shí)間。為了這很短的時(shí)間讓線程掛起,然后轉(zhuǎn)為內(nèi)核態(tài)的時(shí)間可能比鎖定狀態(tài)的時(shí)間更長(zhǎng)。所以,我們可以讓等待同步鎖的進(jìn)程不要進(jìn)入阻塞,而是在原地稍微等待一會(huì)兒,不要放棄處理器的執(zhí)行時(shí)間,看看持有鎖的線程是不是很快就會(huì)釋放鎖。為了讓線程等待,我們可以讓線程執(zhí)行一個(gè)忙循環(huán)(原地自旋),這就是自旋鎖。
?自旋鎖在 JDK1.4.2 之后就已經(jīng)引入,但是默認(rèn)是關(guān)閉的。我們可以使用-XX:+UseSpinning 參數(shù)來開啟。在 JDK1.6 之后就默認(rèn)開啟了。自旋鎖并不是阻塞,所以它避免了用戶態(tài)到內(nèi)核態(tài)的頻繁轉(zhuǎn)化,但是它是要占用處理器的執(zhí)行時(shí)間的。
?如果占有對(duì)象鎖的線程在很短的時(shí)間內(nèi)就執(zhí)行完,然后釋放鎖,這樣的話,自旋鎖的效果就會(huì)非常好。
?如果占有對(duì)象鎖的線程執(zhí)行時(shí)間很長(zhǎng),那么自旋鎖會(huì)白白消耗處理器的執(zhí)行時(shí)間,這就帶來了性能的浪費(fèi)。這樣的話,還不如將等待的線程進(jìn)行阻塞。默認(rèn)的自旋次數(shù)是 10,也就是說,如果一個(gè)線程自旋 10 次之后,還沒有拿到對(duì)象鎖,那么就會(huì)進(jìn)行阻塞。
?我們也可以使用參數(shù)-XX:PreBlockSpin 來更改。
2.自適應(yīng)自旋鎖?
無論是使用默認(rèn)的 10 次,還是用戶自定義的次數(shù),對(duì)整個(gè)虛擬機(jī)來說所有的線程都是一樣的。但是同一個(gè)虛擬機(jī)中線程的狀態(tài)并不是一樣的,有的鎖對(duì)象長(zhǎng)一點(diǎn),有的短一點(diǎn),所以,在 JDK1.6 的時(shí)候,引入了自適應(yīng)自旋鎖。
?自適應(yīng)自旋鎖意味著自旋的時(shí)間不在固定了,而是根據(jù)當(dāng)前的情況動(dòng)態(tài)設(shè)置。
?主要取決于同一個(gè)鎖上一次的自旋時(shí)間和鎖的擁有者的狀態(tài)。
?如果在同一個(gè)對(duì)象鎖上,上一個(gè)獲取這個(gè)對(duì)象鎖的線程在自旋等待成功了,沒有進(jìn)入阻塞狀態(tài),說明這個(gè)對(duì)象鎖的線程執(zhí)行時(shí)間會(huì)很短,虛擬機(jī)認(rèn)為這次也有可能再次成功,進(jìn)而允許此次自旋時(shí)間可以更長(zhǎng)一點(diǎn)。
?如果對(duì)于某個(gè)鎖,自旋狀態(tài)下很少獲得過鎖,說明這個(gè)對(duì)象鎖的線程執(zhí)行時(shí)間相對(duì)會(huì)長(zhǎng)一點(diǎn),那么以后虛擬機(jī)可能會(huì)直接省略掉自旋的過程。避免浪費(fèi)處理器資源。
?自適應(yīng)自旋鎖的加入,隨著程序運(yùn)行時(shí)間的增長(zhǎng)以及性能監(jiān)控系統(tǒng)信息的不斷完善,虛擬機(jī)對(duì)程序的自旋時(shí)間預(yù)測(cè)越來越準(zhǔn)確,也就是虛擬機(jī)越來越聰明了。
鎖消除?
鎖消除指的是,在即時(shí)編譯器運(yùn)行的時(shí)候,代碼中要求某一段代碼塊進(jìn)行互斥同步,但是虛擬機(jī)檢測(cè)到不需要進(jìn)行互斥同步,因?yàn)闆]有共享數(shù)據(jù),此時(shí),虛擬機(jī)會(huì)進(jìn)行優(yōu)化,將互斥同步消除。
?鎖消除的主要判定依據(jù)來源于逃逸分析的數(shù)據(jù)支持。具體來說,如果虛擬機(jī)判斷到,在一段代碼中,創(chuàng)建的對(duì)象不會(huì)逃逸出去到其他線程,那么就可以把他當(dāng)作棧上數(shù)據(jù)對(duì)待,同步也就沒有必要了。
?但是,大家肯定有疑問,變量是否逃逸,寫代碼的程序員應(yīng)該比虛擬機(jī)清楚,該不該加同步互斥程序員很自信。還要讓虛擬機(jī)通過復(fù)雜的過程間分析嗎,這個(gè)問題的答案是:
?有許多互斥同步的要求并不是程序員自己加入的,互斥同步的代碼在 Java 中出現(xiàn)的程度很頻繁。
?我們來舉一個(gè)例子。
public String concat(String s1, String s2){return s1 + s2; }上邊的代碼很簡(jiǎn)單,將兩個(gè)字符串連接,然后返回,不涉及到任何互斥同步的要求。
?但是,我們來編譯它
0 new #2 <java/lang/StringBuilder>3 dup4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>7 aload_18 invokevirtual #4 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;> 11 aload_2 12 invokevirtual #4 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;> 15 invokevirtual #5 <java/lang/StringBuilder.toString : ()Ljava/lang/String;> 18 areturn會(huì)發(fā)現(xiàn),字節(jié)碼中出現(xiàn)了 StringBuilder 的拼接操作。因?yàn)樽址遣豢勺兊?#xff0c;在編譯階段會(huì)對(duì) String 的連接自動(dòng)優(yōu)化。也就是用 StringBuilder 來連接。我們都知道,這個(gè)類是線程安全的,也就是說 StringBuilder 的拼接操作是需要互斥同步的條件的。此時(shí),代碼流程可能是以下這樣的
public String concat(String s1, String s2){StringBuilder sb = new StringBuilder();sb.append(s1);sb.append(s2);return sb.toString(); }此時(shí),代碼會(huì)有互斥同步,鎖住 sb 這個(gè)對(duì)象。這樣的話,就會(huì)出現(xiàn)程序員沒有加入互斥同步條件,但字節(jié)碼中以及有了。
?這個(gè)時(shí)候,鎖消除就發(fā)揮作用了,通過虛擬機(jī)的逃逸分析,發(fā)現(xiàn) sb 這個(gè)對(duì)象不會(huì)逃逸出去,別的線程絕對(duì)不會(huì)訪問到它,sb 的動(dòng)態(tài)作用域就在此方法中,此時(shí),鎖消除就會(huì)將這里的互斥同步進(jìn)行消除。
?運(yùn)行的時(shí)候,就會(huì)忽略到同步措施直接執(zhí)行。
鎖粗化?
原則上,我們編寫代碼的時(shí)候,總是推薦將同步代碼塊的作用范圍盡可能的縮小,只有共享數(shù)據(jù)的地方同步即可。這樣是為了使得同步的操作變少,等待鎖的線程能盡快的拿到鎖。
?但是,如果一段代碼中自始至終都鎖的是同一個(gè)對(duì)象,那么就會(huì)對(duì)這個(gè)對(duì)象進(jìn)行重復(fù)的加鎖,釋放,加鎖,釋放。頻繁的進(jìn)行用戶態(tài)和內(nèi)核態(tài)的切換,效率居然變低了。
?上邊的代碼就是這種情況,每一次的 append 操作都對(duì) sb 進(jìn)行加鎖釋放,加鎖釋放,如果虛擬機(jī)探測(cè)到有一串零碎的操作對(duì)一個(gè)對(duì)象重復(fù)的加鎖,釋放,此時(shí),虛擬機(jī)就會(huì)把加鎖同步的范圍粗化到整個(gè)操作的最外層。以上邊的代碼為例,虛擬機(jī)擴(kuò)展到第一個(gè) append 到最后一個(gè) append。這樣的話,只需要加鎖釋放一次即可。
輕量級(jí)鎖?
輕量級(jí)鎖是 JDK1.6 之后加入的新型鎖機(jī)制,輕量級(jí)是相對(duì)應(yīng)于操作系統(tǒng)互斥量來實(shí)現(xiàn)的傳統(tǒng)鎖而言的。因此,傳統(tǒng)鎖就被稱之為重量級(jí)鎖。但是,要注意,輕量級(jí)并不是用來代替重量級(jí)的,它設(shè)計(jì)的初衷是在沒有多線程競(jìng)爭(zhēng)的前提下,減少傳統(tǒng)的重量級(jí)鎖帶來的性能消耗問題的。
?首先,要理解輕量級(jí)鎖以及后邊的偏向鎖,必須要先知道,HotSpot 中對(duì)象的內(nèi)存布局。對(duì)象的內(nèi)存布局分為三部分,一部分是對(duì)象頭(Mark Word),一部分是實(shí)例數(shù)據(jù),還有一部分對(duì)其填充,為了讓對(duì)象的大小為 8 字節(jié)的整數(shù)倍。
?對(duì)象頭中包括兩部分的數(shù)據(jù)包括,對(duì)象的哈希碼,GC 分代年齡,鎖狀態(tài)等。 如果對(duì)象是數(shù)組,那么還會(huì)有額外的一部分存儲(chǔ)數(shù)組長(zhǎng)度。
?由于對(duì)象頭中存儲(chǔ)的信息是與對(duì)象自身定義數(shù)據(jù)無關(guān)的額外存儲(chǔ)成本,所以為了節(jié)約效率,他被設(shè)計(jì)為一個(gè)動(dòng)態(tài)的數(shù)據(jù)結(jié)構(gòu)。會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間。具體來說,會(huì)根據(jù)當(dāng)前鎖狀態(tài)給每一部分的值賦予不同的意義。
?在 32 位操作系統(tǒng)下的 HotSpot 虛擬機(jī)中對(duì)象頭占用 32 個(gè)字節(jié),64 位占用 64 個(gè)字節(jié)。
?我們以 32 位操作系統(tǒng)來演示。以下是不同鎖狀態(tài)的情況下,各個(gè)部分?jǐn)?shù)據(jù)的含義。
?接下來我們就可以介紹輕量級(jí)鎖的工作過程了。
加鎖過程
1.在代碼即將進(jìn)入同步塊的時(shí)候,虛擬機(jī)就會(huì)在當(dāng)前棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間。然后將堆中對(duì)象的對(duì)象頭拷貝到鎖記錄(官方給它加了 Displaced 前綴)便于修改對(duì)象頭的引用時(shí)存儲(chǔ)之前的信息。此時(shí)線程棧和對(duì)象頭的情況如下:
2.然后,虛擬機(jī)將使用 CAS(原子)操作嘗試把堆中對(duì)象的對(duì)象頭中前 30 個(gè)字節(jié)更新為指向鎖記錄的引用。
- 如果成功,代表當(dāng)前線程已經(jīng)擁有了該對(duì)象的對(duì)象鎖。然后將堆中對(duì)象頭的鎖標(biāo)志位改為 00。此時(shí),代表對(duì)象就處于輕量級(jí)鎖定狀態(tài)。狀態(tài)如下所示
- 如果失敗,也就是堆中對(duì)象頭的鎖狀態(tài)已經(jīng)是 0,則意味著對(duì)象的對(duì)象鎖別拿走了。
1.虛擬機(jī)會(huì)判斷對(duì)象的前 30 個(gè)字節(jié)是不是指向當(dāng)前線程
- 如果是,說明當(dāng)前線程已經(jīng)拿到了對(duì)象鎖,可以直接執(zhí)行同步代碼塊
- 如果不是,說明對(duì)象鎖被其他線程拿走了,必須等待。也就是進(jìn)入自旋模式,如果在自旋一定次數(shù)后仍為獲得鎖,那么輕量級(jí)鎖將會(huì)膨脹成重量級(jí)鎖。
2.如果發(fā)現(xiàn)有兩條以上線程爭(zhēng)用同一個(gè)對(duì)象鎖,那么輕量級(jí)鎖就不在有效,必須膨脹為重量鎖,將對(duì)象的鎖狀態(tài)改為 10。此時(shí),堆中對(duì)象的對(duì)象頭前 30 個(gè)字節(jié)的引用就是指向重量級(jí)鎖。
解鎖過程?
如果堆中對(duì)象頭的前 30 個(gè)字節(jié)指向當(dāng)前線程,說明當(dāng)前線程擁有對(duì)象鎖,就用 CAS 操作將加鎖的時(shí)候復(fù)制到棧幀鎖記錄中的對(duì)象頭替換到堆中對(duì)象的對(duì)象頭。并將堆中對(duì)象頭的鎖狀態(tài)改為 01。
- 如果替換成功,說明解鎖完成。
- 如果發(fā)現(xiàn)有別的線程嘗試過獲取堆中對(duì)象的對(duì)象鎖,就要在釋放鎖的同時(shí),喚醒被阻塞的線程。
后言?
輕量級(jí)鎖提升性能的依據(jù)是:絕大多數(shù)的鎖在整個(gè)同步過程中都是不存在競(jìng)爭(zhēng)的。這樣的話,就通過 CAS 操作避免了使用操作系統(tǒng)中互斥量的開銷。
?如果確實(shí)存在多個(gè)線程的鎖競(jìng)爭(zhēng),除了互斥量本身的開銷之外,還額外發(fā)生了 CAS 操作的開銷。因此,在有競(jìng)爭(zhēng)的情況下,輕量級(jí)鎖會(huì)比傳統(tǒng)的重量級(jí)鎖更慢。
偏向鎖?
偏向鎖也是 JDK1.6 之后引入的特性,他的目的是消除數(shù)據(jù)在無競(jìng)爭(zhēng)狀態(tài)下的同步原語(yǔ),進(jìn)一步提高程序的運(yùn)行速度。
?輕量級(jí)鎖是在無競(jìng)爭(zhēng)的情況下利用 CAS 原子操作來消除操作系統(tǒng)的互斥量,偏向鎖就是在無競(jìng)爭(zhēng)的情況下把整個(gè)同步都消除。
?偏向鎖的偏就是偏心的偏,他的意思是 這個(gè)鎖會(huì)偏向于第一個(gè)獲得它的線程。如果在接下來的執(zhí)行過程中,該鎖一直沒有被其他線程獲取,那么持有偏向鎖的線程就不需要在同步,直接執(zhí)行。
?假設(shè)當(dāng)前虛擬機(jī)開啟了偏向鎖(1.6 之后默認(rèn)開啟),當(dāng)鎖對(duì)象第一次被線程獲取的時(shí)候,虛擬機(jī)會(huì)將對(duì)象頭中最后 2 字節(jié)的鎖標(biāo)志位的值不做設(shè)置,依舊是 01,將倒數(shù)第三個(gè)字節(jié)偏向模式設(shè)置為 01。也就是開啟偏向模式。同時(shí)使用 CAS 原子操作將獲取到這個(gè)對(duì)象鎖的線程記錄在對(duì)象頭中。如果操作成功,那么以后持有偏向鎖的線程每次進(jìn)入同步代碼塊時(shí),虛擬機(jī)都不會(huì)在進(jìn)行同步操作。
?一旦出現(xiàn)別的線程去獲取這個(gè)鎖的情況,偏向模式立馬結(jié)束。根據(jù)鎖對(duì)象目前是否被鎖定來決定是否撤銷偏向,撤銷后鎖標(biāo)志位恢復(fù)到未鎖定狀態(tài)(01)或輕量級(jí)鎖定(00)。后續(xù)的操作就按照輕量級(jí)鎖去執(zhí)行。
?偏向鎖,輕量級(jí)鎖的狀態(tài)轉(zhuǎn)化如下:
問題:?
之前的輕量級(jí)鎖加鎖的時(shí)候,會(huì)將對(duì)象的 hash 碼,分代年齡等數(shù)據(jù)拷貝出來,便于使用。但是,我們發(fā)現(xiàn),偏向鎖的過程中并未拷貝,此時(shí),如果要使用原來對(duì)象頭的數(shù)據(jù),怎么辦?
?虛擬機(jī)的實(shí)現(xiàn)也考慮到了這個(gè)問題。
?對(duì)象的哈希碼并不是創(chuàng)建對(duì)象的時(shí)候計(jì)算的,而是第一次使用的時(shí)候,計(jì)算的。比如下邊 String 的 hash 方法源碼
/**演示hash的計(jì)算時(shí)間作者:杜少雄 */ public int hashCode() {int h = hash;//如果之前沒有算過,則調(diào)用的時(shí)候才進(jìn)行計(jì)算。否則直接返回if (h == 0 && value.length > 0) {char val[] = value;for (int i = 0; i < value.length; i++) {h = 31 * h + val[i];}hash = h;}return h;}?如果一個(gè)對(duì)象計(jì)算過哈希碼,那么不管調(diào)用多少次,它的哈希值都應(yīng)該是一樣的。
?當(dāng)一個(gè)對(duì)象計(jì)算過 hash 碼的時(shí)候,說明這個(gè)對(duì)象的哈希碼要被用,那么,這個(gè)對(duì)象就無法進(jìn)入偏向鎖狀態(tài)。
?如果虛擬機(jī)收到一個(gè)正在偏向鎖的對(duì)象的哈希碼計(jì)算請(qǐng)求,就會(huì)立即停止偏向鎖模式,膨脹為重量級(jí)鎖。就會(huì)在重量級(jí)鎖的棧幀中拷貝的鎖狀態(tài)位置中存儲(chǔ)對(duì)象的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
后言?
偏向鎖可以提高帶有同步但是無競(jìng)爭(zhēng)的程序性能,但是它同樣是一個(gè)帶有權(quán)衡效益的優(yōu)化。如果程序中大多數(shù)的鎖總是被不同的線程訪問,那么偏向模式就是多余的。具體問題分析之后,我們可以使用參數(shù)-XX:-UseBiasedLocking 來禁止使用偏向鎖優(yōu)化從而提高程序的運(yùn)行速度。需要具體問題,具體分析。
文章已同步至 GitHub 開源項(xiàng)目: JVM 底層原理解析
期待你的加入
百度開發(fā)者中心已開啟征稿模式,歡迎開發(fā)者登錄https://developer.baidu.com/?from=020804進(jìn)行投稿,優(yōu)質(zhì)文章將獲得豐厚獎(jiǎng)勵(lì)和推廣資源。
總結(jié)
以上是生活随笔為你收集整理的面试官:你了解JVM的锁优化吗?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微软亚研院:如何看待计算机视觉未来的走向
- 下一篇: Black Hat 2021上的七大网络