深入理解 Synchronized 的鎖優(yōu)化
我們都知道 synchronized 關(guān)鍵字能實(shí)現(xiàn)線程安全,但是你知道這背后的原理是什么嗎?今天我們就來(lái)講一講 synchronized 實(shí)現(xiàn)線程同步背后的原因,以及相關(guān)的鎖優(yōu)化策略吧。
背后的原理
synchronized 關(guān)鍵字經(jīng)過(guò)編譯之后,會(huì)在同步塊的前后分別形成 monitorenter 和 monitorexit 這兩個(gè)字節(jié)碼指令,這兩個(gè)字節(jié)碼只需要一個(gè)指明一個(gè)要鎖定或解鎖的對(duì)象。如果 Java 程序中指明了對(duì)象參數(shù),那么就用這個(gè)對(duì)象作為鎖。
如果沒(méi)有指定,那么就根據(jù) synchronized 修飾的是實(shí)例方法還是類方法,去拿對(duì)應(yīng)的對(duì)象實(shí)例或 Class 對(duì)象來(lái)作為鎖對(duì)象。因此我們可以知道,synchronized 關(guān)鍵字實(shí)現(xiàn)線程同步的背后,其實(shí)是 Java 虛擬機(jī)規(guī)范對(duì)于 monitorenter 和 monitorexit 的定義。
在 Java 虛擬機(jī)規(guī)范對(duì) monitorenter 和 monitorexit 的行為描述中,有兩點(diǎn)需要特別注意。
synchronized 同步塊對(duì)同一條線程是可沖入的,也就是不會(huì)出現(xiàn)自己把自己鎖死的問(wèn)題。
同步課在已進(jìn)入的線程執(zhí)行完之前,會(huì)阻塞后面其他線程的進(jìn)入。
synchronized 關(guān)鍵字在 JDK1.6 版本之前,是通過(guò)操作系統(tǒng)的 Mutex Lock 來(lái)實(shí)現(xiàn)同步的。而操作系統(tǒng)的 Mutex Lock 是操作系統(tǒng)級(jí)別的方法,需要切換到內(nèi)核態(tài)來(lái)執(zhí)行。這就需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài)中,因此我們說(shuō) synchronized 同步是重量級(jí)的操作。
鎖優(yōu)化
在 JDK1.6 版本中,HotSpot 虛擬機(jī)開(kāi)發(fā)團(tuán)隊(duì)花了很大的精力去實(shí)現(xiàn)各種鎖優(yōu)化技術(shù),如:適應(yīng)性自旋、鎖消除、鎖粗話、偏向鎖、輕量級(jí)鎖等。其中最重要的是:自旋鎖、輕量級(jí)鎖、偏向鎖這三個(gè),我們重點(diǎn)講這三個(gè)鎖優(yōu)化。
自旋鎖與自適應(yīng)自旋
對(duì)于重量級(jí)的同步操作來(lái)說(shuō),最大的消耗其實(shí)是內(nèi)核態(tài)與用戶態(tài)的切換。很很多時(shí)候,對(duì)于共享數(shù)據(jù)的操作時(shí)間可能很短,比內(nèi)核態(tài)切換到用戶態(tài)這個(gè)耗時(shí)還短。
于是有人就想:如果有多個(gè)線程并發(fā)去獲取鎖的時(shí)候,如果能讓后面那個(gè)請(qǐng)求鎖的線程「稍等一下」,不放棄 CPU 的執(zhí)行時(shí)間,看看持有鎖的線程是否會(huì)很快釋放鎖。為了讓線程等待,我們只需讓線程執(zhí)行一個(gè)忙循環(huán)(自旋),這項(xiàng)技術(shù)就是所謂的自旋鎖。 從理論上來(lái)看,如果所有線程都很快地獲取鎖、釋放鎖,那么自旋鎖是可以帶來(lái)較大的性能提升的。自旋鎖在 JDK 1.4.2 中就已經(jīng)引入,默認(rèn)自旋 10 次。但自旋鎖默認(rèn)是關(guān)閉的,在 JDK 1.6 中才改為默認(rèn)開(kāi)啟了。
自旋等待雖然避免了線程切換的開(kāi)銷(xiāo),但還是要占用處理器的時(shí)間。如果鎖被占用的時(shí)間段,自旋等待的效果就會(huì)非常好。但如果鎖被長(zhǎng)時(shí)間占用,那么自旋的線程就會(huì)白白消耗處理器的資源,從而帶來(lái)性能上的浪費(fèi)。
為了解決特殊情況下自旋鎖的性能消耗問(wèn)題,在 JDK1.6 的時(shí)候引入了自適應(yīng)的自旋鎖。 自適應(yīng)意味著自旋時(shí)間不再固定,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者狀態(tài)決定。如果在同一鎖對(duì)象上,自旋等待剛剛成功獲得過(guò)鎖,那么虛擬機(jī)認(rèn)為這次自旋也很有可能再次成功,進(jìn)而允許線程自旋更長(zhǎng)時(shí)間,例如自旋 100 個(gè)循環(huán)。
但如果對(duì)于某個(gè)鎖,自旋很少成功獲得過(guò)。那虛擬機(jī)為了避免浪費(fèi) CPU 資源,有可能省略掉自旋過(guò)程。有了自旋鎖,隨著程序運(yùn)行和性能監(jiān)控信息的不斷完善,虛擬機(jī)對(duì)鎖的狀態(tài)預(yù)測(cè)就越準(zhǔn),虛擬機(jī)也會(huì)變得越來(lái)越聰明。
輕量級(jí)鎖
輕量級(jí)鎖是 JDK1.6 加入的新型鎖機(jī)制,名字中的「輕量級(jí)」是相對(duì)于操作系統(tǒng)互斥量這個(gè)重量級(jí)鎖而言的。輕量級(jí)鎖誕生的原因,是由于對(duì)于絕大部分的鎖而言,整個(gè)同步周期都不存在競(jìng)爭(zhēng)。如果沒(méi)有競(jìng)爭(zhēng)的話,那就沒(méi)必要使用重量級(jí)鎖了,于是就誕生了輕量級(jí)鎖來(lái)提高效率。
對(duì)于輕量級(jí)鎖來(lái)說(shuō),其同步的流程如下:
在代碼進(jìn)入同步塊的時(shí)候,如果此同步對(duì)象沒(méi)有被鎖定(鎖標(biāo)志位為 01 狀態(tài)),那么虛擬機(jī)會(huì)在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的 Mark Word 拷貝。
虛擬機(jī)將使用 CAS 操作嘗試將對(duì)象的 Mark Word 更新為指向 Lock Record 的指針。如果更新動(dòng)作成功了,那么線程就泳衣了該對(duì)象的鎖,并且對(duì)象 Mark Word 的鎖標(biāo)志位就變成了 00,表示此對(duì)象處于輕量級(jí)鎖定狀態(tài)。
簡(jiǎn)單地說(shuō),輕量級(jí)鎖的同步流程可以總結(jié)為:使用 CAS 操作,在線程棧幀與鎖對(duì)象建立雙向的指針。
在沒(méi)有線程競(jìng)爭(zhēng)的情況下,輕量級(jí)鎖使用 CAS 自旋操作避免了使用互斥量的開(kāi)銷(xiāo),提高了效率。但如果存在鎖競(jìng)爭(zhēng),除了互斥量的開(kāi)銷(xiāo)外,還額外發(fā)生了 CAS 操作。因此在有競(jìng)爭(zhēng)的情況下,輕量級(jí)鎖會(huì)比傳統(tǒng)的重量級(jí)鎖更慢。
偏向鎖
偏向鎖是 JDK1.6 中引入的一項(xiàng)優(yōu)化,它的意思是這個(gè)鎖會(huì)偏向于第一個(gè)獲得它的線程。如果在接下來(lái)的執(zhí)行過(guò)程中,該鎖沒(méi)有被其他線程獲取,則持有偏向鎖的線程將永遠(yuǎn)不需要再進(jìn)行同步。 對(duì)于偏向鎖來(lái)說(shuō),其同步流程如下所示:
- 假設(shè)當(dāng)前虛擬機(jī)啟動(dòng)了偏向鎖,那么當(dāng)鎖對(duì)象第一次被線程獲取的時(shí)候,虛擬機(jī)將會(huì)把對(duì)象的鎖標(biāo)志位設(shè)置為 01,偏向鎖位設(shè)置為 1。同時(shí)使用 CAS 操作將線程 ID 記錄在對(duì)象的 MarkWord 之中。如果 CAS 操作成功,那么持有偏向鎖的線程進(jìn)入鎖對(duì)應(yīng)的同步塊時(shí),虛擬機(jī)將不再進(jìn)行任何同步操作。
- 當(dāng)有另外一個(gè)線程嘗試去獲取這個(gè)鎖時(shí),根據(jù)鎖對(duì)象目前是否處于鎖定狀態(tài),將其恢復(fù)到未鎖定(01)或輕量級(jí)鎖定(00)狀態(tài)。隨后的同步操作,就向上面介紹的輕量級(jí)鎖那樣執(zhí)行。
可以看到偏向鎖還是需要做一些 CAS 操作,但是對(duì)比起輕量級(jí)鎖來(lái)說(shuō),其要設(shè)置的內(nèi)容大大減少了,因此也提高了一些效率。
偏向鎖可以提高帶有同步但無(wú)競(jìng)爭(zhēng)的程序性能。 它同樣是一個(gè)帶有效益權(quán)衡(Trade Off)性質(zhì)的優(yōu)化,也就是說(shuō),它并不一定總是對(duì)程序運(yùn)行有利,如果程序中大多數(shù)的鎖總是被多個(gè)不同的線程訪問(wèn),那偏向模式就是多余的。
優(yōu)化后的鎖獲取流程
經(jīng)過(guò) JDK1.6 的優(yōu)化,synchronized 同步機(jī)制的流程變成了:
- 首先,synchronized 會(huì)嘗試使用偏向鎖的方式去競(jìng)爭(zhēng)鎖資源,如果能夠競(jìng)爭(zhēng)到偏向鎖,表示加鎖成功直接返回。
- 如果競(jìng)爭(zhēng)鎖失敗,說(shuō)明當(dāng)前鎖已經(jīng)偏向了其他線程。需要將鎖升級(jí)到輕量級(jí)鎖,在輕量級(jí)鎖狀態(tài)下,競(jìng)爭(zhēng)鎖的線程根據(jù)自適應(yīng)自旋次數(shù)去嘗試搶占鎖資源。
- 如果在輕量級(jí)鎖狀態(tài)下還是沒(méi)有競(jìng)爭(zhēng)到鎖,就只能升級(jí)到重量級(jí)鎖。在重量級(jí)鎖狀態(tài)下,沒(méi)有競(jìng)爭(zhēng)到鎖的線程就會(huì)被阻塞。處于鎖等待狀態(tài)的線程需要等待獲得鎖的線程來(lái)觸發(fā)喚醒。
上面的鎖獲取流程,可以用如下的示意圖來(lái)表示:
Java 對(duì)象鎖競(jìng)爭(zhēng)流程
總結(jié)
本文首先簡(jiǎn)單講解了 synchronized 關(guān)鍵字實(shí)現(xiàn)同步的原理,其實(shí)是通過(guò) Java 虛擬機(jī)規(guī)范對(duì)于 monitorenter 和 monitorexit 的支持,從而使得 synchronized 能夠?qū)崿F(xiàn)同步。而 synchronized 同步本質(zhì)上是通過(guò)操作系統(tǒng)的 mutex 鎖來(lái)實(shí)現(xiàn)的。由于操作操作系統(tǒng) mutex 鎖太過(guò)于消耗資源,因此在 JDK1.6 后 HotSpot 虛擬機(jī)做了一系列的鎖優(yōu)化,其中最重要的便是:自旋鎖、輕量級(jí)鎖、偏向鎖。這三個(gè)鎖的誕生原因,以及提升的點(diǎn)如下表所示。
現(xiàn)狀 | 鎖名稱 | 收益 | 使用場(chǎng)景 |
大多數(shù)情況下,等待鎖的時(shí)間比操作系統(tǒng) mutex 短得多 | 自旋鎖 | 減少內(nèi)核態(tài)與用戶態(tài)切換的開(kāi)銷(xiāo) | 線程獲取鎖時(shí)間較短的情況 |
大多數(shù)情況下,鎖同步期間沒(méi)有線程競(jìng)爭(zhēng) | 輕量級(jí)鎖 | 與自旋鎖相比,減少了自旋時(shí)間 | 沒(méi)有線程競(jìng)爭(zhēng)鎖 |
大多數(shù)情況下,鎖同步期間沒(méi)有線程競(jìng)爭(zhēng) | 偏向鎖 | 與輕量級(jí)鎖相比,減少了多余的對(duì)象復(fù)制操作 | 沒(méi)有線程競(jìng)爭(zhēng)鎖 |
從上面表格可以看到,自旋鎖、輕量級(jí)鎖、偏向鎖,他們的優(yōu)化是逐漸深入的。
- 對(duì)于重量級(jí)鎖來(lái)說(shuō),自旋鎖減少了互斥量的內(nèi)核、用戶態(tài)切換開(kāi)銷(xiāo)。
- 對(duì)于自旋鎖來(lái)說(shuō),輕量級(jí)鎖減少了自旋等待的時(shí)間。
- 對(duì)于輕量級(jí)鎖來(lái)說(shuō),偏向于減少了多余的對(duì)象復(fù)制操作。