Java 線程優(yōu)化 偏向鎖,輕量級(jí)鎖、重量級(jí)鎖課程

Java 6 對(duì) synchronized 鎖做了多方面的優(yōu)化,其中最主要的就是引入了 偏向鎖和輕量級(jí)鎖。鎖的獲取次序依次是?偏向鎖?->?輕量級(jí)鎖?-> 重量級(jí)鎖。

synchronized 實(shí)現(xiàn)原理

要了解 synchronized 的原理需要先理清楚兩件事情:對(duì)象頭和 Monitor。

對(duì)象頭

Java對(duì)象在內(nèi)存中分為三部分:對(duì)象頭、實(shí)例數(shù)據(jù)、對(duì)齊填充。當(dāng)我們?cè)?Java 代碼中,使用 new 創(chuàng)建一個(gè)對(duì)象的時(shí)候,JVM 會(huì)在堆中創(chuàng)建一個(gè) instanceOopDesc 對(duì)象,這個(gè)對(duì)象中包含了對(duì)象頭以及實(shí)例數(shù)據(jù)。

instanceOopDesc 的基類為 oopDesc 類。它的結(jié)構(gòu)如下:


oopDesc 結(jié)構(gòu)

其中 _mark 和 _metadata 一起組成了對(duì)象頭。_metadata 主要保存了類元數(shù)據(jù)。重點(diǎn)看下 _mark 屬性,_mark 是 markOop 類型數(shù)據(jù),一般稱它為標(biāo)記字段(Mark Word),其中主要存儲(chǔ)了對(duì)象的 hashCode、分代年齡、鎖標(biāo)志位,是否偏向鎖等。

用一張圖來(lái)表示 32 位 Java 虛擬機(jī)的 Mark Word 的默認(rèn)存儲(chǔ)結(jié)構(gòu)如下:


32 位 Java 虛擬機(jī)的 Mark Word 的默認(rèn)存儲(chǔ)結(jié)構(gòu)??

默認(rèn)情況下,沒(méi)有線程進(jìn)行加鎖操作,所以鎖對(duì)象中的 Mark Word 處于無(wú)鎖狀態(tài)。但是考慮到 JVM 的空間效率,Mark Word 被設(shè)計(jì)成為一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu),以便存儲(chǔ)更多的有效數(shù)據(jù),它會(huì)根據(jù)對(duì)象本身的狀態(tài)復(fù)用自己的存儲(chǔ)空間,如 32 位 JVM 下,除了上述列出的 Mark Word 默認(rèn)存儲(chǔ)結(jié)構(gòu)外,還有如下可能變化的結(jié)構(gòu):


不同情況下Mark Word 的結(jié)構(gòu)

從圖中可以看出,根據(jù)"鎖標(biāo)志位”以及"是否為偏向鎖",Java 中的鎖可以分為以下幾種狀態(tài):


Java 中的鎖的幾種狀態(tài)

在 Java 6 之前,并沒(méi)有輕量級(jí)鎖和偏向鎖,只有重量級(jí)鎖,也就是通常所說(shuō) synchronized 的對(duì)象鎖,鎖標(biāo)志位為 10。從圖中的描述可以看出:當(dāng)鎖是重量級(jí)鎖時(shí),對(duì)象頭中 Mark Word 會(huì)用 30 bit 來(lái)指向一個(gè)“互斥量”,而這個(gè)互斥量就是 Monitor。

Monitor

Monitor 可以把它理解為一個(gè)同步工具,也可以描述為一種同步機(jī)制。實(shí)際上,它是一個(gè)保存在對(duì)象頭中的一個(gè)對(duì)象。在 markOop 中有如下代碼:

通過(guò) monitor() 方法創(chuàng)建一個(gè) ObjectMonitor 對(duì)象,而 ObjectMonitor 就是 Java 虛擬機(jī)中的 Monitor 的具體實(shí)現(xiàn)。因此 Java 中每個(gè)對(duì)象都會(huì)有一個(gè)對(duì)應(yīng)的 ObjectMonitor 對(duì)象,這也是 Java 中所有的 Object 都可以作為鎖對(duì)象的原因。

ObjectMonitor 是如何實(shí)現(xiàn)同步機(jī)制

首先看下 ObjectMonitor 的結(jié)構(gòu):

ObjectMonitor 結(jié)構(gòu)

其中有幾個(gè)比較關(guān)鍵的屬性:

當(dāng)多個(gè)線程同時(shí)訪問(wèn)一段同步代碼時(shí),首先會(huì)進(jìn)入 _EntryList 隊(duì)列中,當(dāng)某個(gè)線程通過(guò)競(jìng)爭(zhēng)獲取到對(duì)象的 monitor 后,monitor 會(huì)把 _owner 變量設(shè)置為當(dāng)前線程,同時(shí) monitor 中的計(jì)數(shù)器 _count 加 1,即獲得對(duì)象鎖。

若持有 monitor 的線程調(diào)用 wait() 方法,將釋放當(dāng)前持有的 monitor,_owner 變量恢復(fù)為 null, _count 自減 1,同時(shí)該線程進(jìn)入 _WaitSet 集合中等待被喚醒。若當(dāng)前線程執(zhí)行完畢也將釋放 monitor(鎖)并復(fù)位變量的值,以便其他線程進(jìn)入獲取 monitor(鎖)。

實(shí)例演示

比如以下代碼通過(guò) 3 個(gè)線程分別執(zhí)行以下同步代碼塊:

鎖對(duì)象是 lock 對(duì)象,在 JVM 中會(huì)有一個(gè) ObjectMonitor 對(duì)象與之對(duì)應(yīng)。如下圖所示:

分別使用 3 個(gè)線程來(lái)執(zhí)行以上同步代碼塊。默認(rèn)情況下,3 個(gè)線程都會(huì)先進(jìn)入 ObjectMonitor 中的 EntrySet 隊(duì)列中,如下所示:

假設(shè)線程 2 首先通過(guò)競(jìng)爭(zhēng)獲取到了鎖對(duì)象,則 ObjectMonitor 中的 Owner 指向線程 2,并將 count 加 1。結(jié)果如下:

上圖中 Owner 指向線程 2 表示它已經(jīng)成功獲取到鎖(Monitor)對(duì)象,其他線程只能處于阻塞(blocking)狀態(tài)。如果線程 2 在執(zhí)行過(guò)程中調(diào)用 wait() 操作,則線程 2 會(huì)釋放鎖(Monitor)對(duì)象,以便其他線程進(jìn)入獲取鎖(Monitor)對(duì)象,Owner 變量恢復(fù)為 null,count 做減 1 操作,同時(shí)線程 2 會(huì)添加到 WaitSet 集合,進(jìn)入等待(waiting)狀態(tài)并等待被喚醒。結(jié)果如下:

然后線程 1 和線程 3 再次通過(guò)競(jìng)爭(zhēng)獲取到鎖(Monitor)對(duì)象,則重新將 Owner 指向成功獲取到鎖的線程。假設(shè)線程 1 獲取到鎖,如下:

當(dāng)線程 1 中的代碼執(zhí)行完畢以后,同樣會(huì)自動(dòng)釋放鎖,以便其他線程再次獲取鎖對(duì)象。

實(shí)際上,ObjectMonitor 的同步機(jī)制是 JVM 對(duì)操作系統(tǒng)級(jí)別的 Mutex Lock(互斥鎖)的管理過(guò)程,其間都會(huì)轉(zhuǎn)入操作系統(tǒng)內(nèi)核態(tài)。也就是說(shuō) synchronized 實(shí)現(xiàn)鎖,在“重量級(jí)鎖”狀態(tài)下,當(dāng)多個(gè)線程之間切換上下文時(shí),還是一個(gè)比較重量級(jí)的操作。


Java 虛擬機(jī)對(duì) synchronized 的優(yōu)化

從 Java 6 開始,虛擬機(jī)對(duì) synchronized 關(guān)鍵字做了多方面的優(yōu)化,主要目的就是,避免 ObjectMonitor 的訪問(wèn),減少“重量級(jí)鎖”的使用次數(shù),并最終減少線程上下文切換的頻率 。其中主要做了以下幾個(gè)優(yōu)化: 鎖自旋、輕量級(jí)鎖、偏向鎖。

鎖自旋

線程的阻塞和喚醒需要 CPU 從用戶態(tài)轉(zhuǎn)為核心態(tài),頻繁的阻塞和喚醒對(duì) CPU 來(lái)說(shuō)是一件負(fù)擔(dān)很重的工作,勢(shì)必會(huì)給系統(tǒng)的并發(fā)性能帶來(lái)很大的壓力,所以 Java 引入了自旋鎖的操作。實(shí)際上自旋鎖在 Java 1.4 就被引入了,默認(rèn)關(guān)閉,但是可以使用參數(shù) -XX:+UseSpinning 將其開啟。但是從 Java 6 之后默認(rèn)開啟。

所謂自旋,就是讓該線程等待一段時(shí)間,不會(huì)被立即掛起,看當(dāng)前持有鎖的線程是否會(huì)很快釋放鎖。而所謂的等待就是執(zhí)行一段無(wú)意義的循環(huán)即可(自旋)。

自旋鎖也存在一定的缺陷:自旋鎖要占用 CPU,如果鎖競(jìng)爭(zhēng)的時(shí)間比較長(zhǎng),那么自旋通常不能獲得鎖,白白浪費(fèi)了自旋占用的 CPU 時(shí)間。這通常發(fā)生在鎖持有時(shí)間長(zhǎng),且競(jìng)爭(zhēng)激烈的場(chǎng)景中,此時(shí)應(yīng)主動(dòng)禁用自旋鎖。


輕量級(jí)鎖

有時(shí)候 Java 虛擬機(jī)中會(huì)存在這種情形:對(duì)于一塊同步代碼,雖然有多個(gè)不同線程會(huì)去執(zhí)行,但是這些線程是在不同的時(shí)間段交替請(qǐng)求這把鎖對(duì)象,也就是不存在鎖競(jìng)爭(zhēng)的情況。在這種情況下,鎖會(huì)保持在輕量級(jí)鎖的狀態(tài),從而避免重量級(jí)鎖的阻塞和喚醒操作。

要了解輕量級(jí)鎖的工作流程,還是需要再次看下對(duì)象頭中的 Mark Word。上文中已經(jīng)提到,鎖的標(biāo)志位包含幾種情況:00 代表輕量級(jí)鎖、01 代表無(wú)鎖(或者偏向鎖)、10 代表重量級(jí)鎖、11 則跟垃圾回收算法的標(biāo)記有關(guān)。

當(dāng)線程執(zhí)行某同步代碼時(shí),Java 虛擬機(jī)會(huì)在當(dāng)前線程的棧幀中開辟一塊空間(Lock Record)作為該鎖的記錄,如下圖所示:

然后 Java 虛擬機(jī)會(huì)嘗試使用 CAS(Compare And Swap)操作,將鎖對(duì)象的 Mark Word 拷貝到這塊空間中,并且將鎖記錄中的 owner 指向 Mark Word。結(jié)果如下:

當(dāng)線程再次執(zhí)行此同步代碼塊時(shí),判斷當(dāng)前對(duì)象的 Mark Word 是否指向當(dāng)前線程的棧幀,如果是則表示當(dāng)前線程已經(jīng)持有當(dāng)前對(duì)象的鎖,則直接執(zhí)行同步代碼塊;否則只能說(shuō)明該鎖對(duì)象已經(jīng)被其他線程搶占了,這時(shí)輕量級(jí)鎖需要膨脹為重量級(jí)鎖。

輕量級(jí)鎖所適應(yīng)的場(chǎng)景是線程交替執(zhí)行同步塊的場(chǎng)合,如果存在同一時(shí)間訪問(wèn)同一鎖的場(chǎng)合,就會(huì)導(dǎo)致輕量級(jí)鎖膨脹為重量級(jí)鎖。


偏向鎖

輕量級(jí)鎖是在沒(méi)有鎖競(jìng)爭(zhēng)情況下的鎖狀態(tài),但是在有些時(shí)候鎖不僅存在多線程的競(jìng)爭(zhēng),而且總是由同一個(gè)線程獲得。因此為了讓線程獲得鎖的代價(jià)更低引入了偏向鎖的概念。偏向鎖的意思是如果一個(gè)線程獲得了一個(gè)偏向鎖,如果在接下來(lái)的一段時(shí)間中沒(méi)有其他線程來(lái)競(jìng)爭(zhēng)鎖,那么持有偏向鎖的線程再次進(jìn)入或者退出同一個(gè)同步代碼塊,不需要再次進(jìn)行搶占鎖和釋放鎖的操作。偏向鎖可以通過(guò) -XX:+UseBiasedLocking 開啟或者關(guān)閉。

偏向鎖的具體實(shí)現(xiàn)就是在鎖對(duì)象的對(duì)象頭中有個(gè) ThreadId 字段,默認(rèn)情況下這個(gè)字段是空的,當(dāng)?shù)谝淮潍@取鎖的時(shí)候,就將自身的 ThreadId 寫入鎖對(duì)象的 Mark Word 中的 ThreadId 字段內(nèi),將是否偏向鎖的狀態(tài)置為 01。這樣下次獲取鎖的時(shí)候,直接檢查 ThreadId 是否和自身線程 Id 一致,如果一致,則認(rèn)為當(dāng)前線程已經(jīng)獲取了鎖,因此不需再次獲取鎖,略過(guò)了輕量級(jí)鎖和重量級(jí)鎖的加鎖階段。提高了效率。

其實(shí)偏向鎖并不適合所有應(yīng)用場(chǎng)景, 因?yàn)橐坏┏霈F(xiàn)鎖競(jìng)爭(zhēng),偏向鎖會(huì)被撤銷,并膨脹成輕量級(jí)鎖,而撤銷操作(revoke)是比較重的行為,只有當(dāng)存在較多不會(huì)真正競(jìng)爭(zhēng)的 synchronized 塊時(shí),才能體現(xiàn)出明顯改善;因此實(shí)踐中,還是需要考慮具體業(yè)務(wù)場(chǎng)景,并測(cè)試后,再?zèng)Q定是否開啟/關(guān)閉偏向鎖。

最后

本課程主要介紹了 Java 中鎖的幾種狀態(tài),其中偏向鎖和輕量級(jí)鎖都是通過(guò)自旋等技術(shù)避免真正的加鎖,而重量級(jí)鎖才是獲取鎖和釋放鎖,重量級(jí)鎖通過(guò)對(duì)象內(nèi)部的監(jiān)視器(ObjectMonitor)實(shí)現(xiàn),其本質(zhì)是依賴于底層操作系統(tǒng)的 Mutex Lock 實(shí)現(xiàn),操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài)的切換,成本非常高。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容