今天我們來(lái)聊聊 Synchronized 里面的各種鎖:偏向鎖、輕量級(jí)鎖、重量級(jí)鎖,以及三個(gè)鎖之間是如何進(jìn)行鎖膨脹的。

眾所周知,線程阻塞帶來(lái)的上下文切換的代價(jià)是很大的,Java 為了盡量減少上下文的切換從而引入了更多的鎖機(jī)制。在了解各種鎖機(jī)制之前,先要學(xué)習(xí)一些前置知識(shí)。對(duì)于各種鎖的獲取和釋放、以及鎖升級(jí)的流程,在文末總結(jié)處有一張圖,如果趕時(shí)間,直接看圖吧。
Mark Word

Java 對(duì)象頭里面有一部分叫做 Mark Word,在32位虛擬機(jī)下面占有 32bit,在 64 位虛擬機(jī)下面占用 64 bit,本文以 32 位虛擬機(jī)為例子。
鎖介紹
偏向鎖:一個(gè)線程反復(fù)的去獲取/釋放一個(gè)鎖,如果這個(gè)鎖是輕量級(jí)鎖或者重量級(jí)鎖,不斷的加解鎖顯然是沒(méi)有必要的,造成了資源的浪費(fèi)。于是引入了偏向鎖,偏向鎖在獲取資源的時(shí)候會(huì)在資源對(duì)象上記錄該對(duì)象是偏向該線程的,偏向鎖并不會(huì)主動(dòng)釋放,這樣每次偏向鎖進(jìn)入的時(shí)候都會(huì)判斷改資源是否是偏向自己的,如果是偏向自己的則不需要進(jìn)行額外的操作,直接可以進(jìn)入同步操作。
輕量級(jí)鎖:輕量級(jí)鎖是由偏向所升級(jí)來(lái)的,偏向鎖運(yùn)行在一個(gè)線程進(jìn)入同步塊的情況下,當(dāng)?shù)诙€(gè)線程加入鎖爭(zhēng)用的時(shí)候,偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖; 在輕量級(jí)鎖中如果發(fā)生多線程競(jìng)爭(zhēng),未持有鎖的線程會(huì)自旋等待。
重量級(jí)鎖:由操作系統(tǒng)的 mutex 來(lái)實(shí)現(xiàn),多線程競(jìng)爭(zhēng)下,未持有鎖的線程將被阻塞。
鎖的流轉(zhuǎn)

對(duì)象分配
首先確定該類的偏向鎖是否可用(決定了在新建對(duì)象實(shí)例的時(shí)候倒數(shù)第三 bit 是 0 還是 1);如果不可用,直接使用輕量鎖。如果偏向鎖可用,新建實(shí)例對(duì)象 obj,此時(shí) obj 進(jìn)入到未鎖定、未偏向、可偏向的狀態(tài)。
偏向鎖 初始鎖定
此時(shí)線程 A 想要獲取對(duì)象 obj 的偏向鎖,由于此時(shí) obj 沒(méi)有偏向任何線程(有可能是剛剛新建,也有可能是由鎖定狀態(tài)重偏向之后導(dǎo)致的未鎖定狀態(tài)),所以利用 CAS 操作將線程 ID 寫入到 Mark Word 里面,此時(shí) 線程 A 獲取了 obj 的偏向鎖。obj 處于已偏向、鎖定狀態(tài)。
偏向鎖 鎖定/解鎖
一旦 obj 第一次偏向了線程 A,A 就可以在沒(méi)有競(jìng)爭(zhēng)的情況下,也就是鎖不升級(jí)的情況下,以極小的代價(jià)反復(fù)獲取 obj 對(duì)象的鎖。
偏向鎖 重偏向(rebias)
如果 obj 先被線程 A 鎖定,然后釋放。然后線程 B 過(guò)來(lái)是否能重新獲取偏向鎖吶?在一定條件下,經(jīng)過(guò)了重偏向可以重新獲得偏向鎖。
通過(guò)上面的 mark word 我們可以看出,在偏向鎖的時(shí)候有一個(gè)字段是 epoch,同時(shí)在 obj 的類 O 信息里面也有一個(gè) epoch,每次系統(tǒng)到達(dá)安全點(diǎn)會(huì)對(duì)類的 epoch 加 1,變成 epoch_new,然后掃描所有的類 O 的實(shí)例,判斷該偏向鎖是否還被持有,如果被持有則將 epoch_new 復(fù)制給對(duì)象頭的 epoch 字段。
每次去獲取偏向鎖的時(shí)候回去判斷對(duì)象實(shí)例的 epoch 和 類的 epoch 是否相等,如果不等代表對(duì)象是未鎖定、可偏向、未偏向狀態(tài),可以偏向新的線程。
撤銷偏向(revoke)
Once biased, that thread can subsequently lock and unlock the object without resorting to expensive atomic instructions. Obviously, an object can be biased toward at most one thread at any given time. (We refer to that thread as the bias holding thread). If another thread tries to acquire a biased object, however, we need to revoke the bias from the original thread.
線程 B 來(lái)競(jìng)爭(zhēng),此時(shí)偏向鎖會(huì)膨脹位輕量級(jí)鎖。當(dāng) B 線程想利用 CAS 獲取偏向鎖失敗, A 線程被暫停,然后檢查 A 線程;
A 線程已經(jīng)退出了同步代碼塊,或者是已經(jīng)不在存活了,如果是上面兩種情況之一的,將 obj 的 Mark Word 后 3bit 改為 001(無(wú)鎖狀態(tài)),然后再去喚醒 A 線程。
A 線程還在同步代碼塊中,此時(shí)將 A 線程的偏向鎖升級(jí)為輕量級(jí)鎖。首先會(huì) copy 一份 obj 對(duì)象的 mark word 內(nèi)容,放到線程 A 當(dāng)前棧幀的一個(gè)叫做 Lock Record 空間的 displace mark word 的位置,

然后通過(guò) CAS 將 obj 中 mark word 的內(nèi)容替換為指向棧幀中 mark word 的位置,最后將 Lock Record 里面的 owner 指向 obj 對(duì)象。并且對(duì)象Mark Word的鎖標(biāo)志位設(shè)置為“00”,到此完成了鎖的升級(jí)。

Bulk Revoke
Java 會(huì)在類信息里面維護(hù)一個(gè)計(jì)數(shù)器記錄,記錄該類發(fā)生偏向鎖 revoke 的次數(shù),一旦達(dá)到某個(gè)閾值,則認(rèn)為這個(gè)類不適合偏向鎖,將會(huì)禁用這個(gè)類的偏向鎖功能。這個(gè)類新建的對(duì)象實(shí)例的最后 3bit 將是 001(無(wú)鎖)。
Objects that are explicitly designed to be shared between multiple threads, such as producer/consumer queues, are not suitable for biased locking. Therefore, biased locking is disabled for a class if revocations for its instances happened frequently in the past. This is called bulk revocation. If the locking code is invoked on an instance of a class for which biased locking was disabled, it performs the standard thin locking. Newly allocated instances of the class are marked as non-biasable.
輕量級(jí)鎖定
剛剛在上文中關(guān)于撤銷偏向(revoke)中已經(jīng)描述了鎖定的流程。在此再總結(jié)一遍
- 復(fù)制 mark word 到 displace mark word
- CAS 替換 mark word 中的信息為指針
- 修改 鎖 標(biāo)志為 00
- 將棧幀中的 owner 指向鎖定的對(duì)象
輕量級(jí)鎖 遞歸鎖定

線程 A 在去獲取輕量級(jí)鎖的時(shí)候,會(huì)首先使用 CAS 操作,如果操作失敗那么會(huì)在此時(shí)判斷是不是該線程已經(jīng)持有過(guò)該對(duì)象的鎖了,通過(guò)判斷對(duì)象的 mark word 是不是指向當(dāng)前線程的棧幀,如果是則會(huì)在最新的棧幀處新建一個(gè) displaced mark word 為 null 的 lock record。關(guān)于為什么要這么做,其實(shí)就是為了記錄一下鎖的重入,發(fā)生重入了需要記錄,本來(lái)是記錄到 mark word 里面最方便,可能是 mark word 沒(méi)有足夠的空間。如果是在 lock record里面記錄的話,需要遍歷 lock record 才可以獲取這個(gè)數(shù)量。所以最終Hotspot選擇每次獲得鎖都添加一個(gè)Lock Record來(lái)表示鎖的重入。
類似的遞歸的解鎖只需要將棧幀中的 lock records 刪除即可。
輕量級(jí)鎖 膨脹
線程 A 持有對(duì)象 obj 的輕量級(jí)鎖,線程 B 過(guò)來(lái) CAS 失敗,開始自旋等待,自旋到達(dá)一定次數(shù)之后如果還沒(méi)有獲取該輕量級(jí)鎖,則會(huì)將該鎖膨脹為重量級(jí)鎖。會(huì)將 mark word 的的鎖標(biāo)志為改為10,同時(shí)經(jīng)指針指向互斥量,然后線程 B 掛起。
JDK1.6中 -XX:+UseSpinning開啟; -XX:PreBlockSpin=10 為自旋次數(shù)。
JDK1.7后,去掉 -XX:PreBlockSpin 參數(shù),由jvm控制。
輕量級(jí)鎖 解鎖
輕量級(jí)鎖解鎖也是理由 CAS 將 mark word 里面的指針替換為無(wú)鎖的 mark word 信息。需要判斷 mark word 里面是指向該線程的 lock record
- 如果不是,說(shuō)明已經(jīng)鎖膨脹了,CAS 失敗,此時(shí)需要喚醒在等待重量級(jí)鎖的線程
- 如果是,說(shuō)明鎖沒(méi)有膨脹,直接 CAS 操作將 mark word 改為 001(無(wú)鎖)狀態(tài),為下一個(gè)線程獲取輕量級(jí)鎖做好準(zhǔn)備。
重量級(jí)鎖 解鎖
釋放 mutex,喚醒在等待的線程
總結(jié)
新標(biāo)簽頁(yè)打開圖片可看大圖

參考
java 中的鎖 -- 偏向鎖、輕量級(jí)鎖、自旋鎖、重量級(jí)鎖 - zqz_zqz的博客 - CSDN博客
Java 8 并發(fā)篇 - 冷靜分析 Synchronized(下) - 知乎
Biased Locking in HotSpot | Oracle David Dice's Blog //rebias
Java中的偏向鎖,輕量級(jí)鎖, 重量級(jí)鎖解析 - 神評(píng)網(wǎng) //rebias