【死磕 Java 并發(fā)】----- synchronized的鎖膨脹過(guò)程
synchronized 是 Java 面試的常客,我們需要掌握它的基本使用,比如同步代碼塊、同步普通方法、同步靜態(tài)方法,以及他們的區(qū)別,當(dāng)然這是最初級(jí)的。高級(jí)點(diǎn)的就是需要掌握 synchronized 的實(shí)現(xiàn)原理,比如對(duì)象頭、synchronized 的鎖優(yōu)化、鎖的膨脹過(guò)程,這篇文章就是介紹 synchronized 的鎖膨脹過(guò)程。該過(guò)程其實(shí)很多小伙伴都不知道,18年面試的時(shí)候問(wèn)了不下于 20 個(gè)人,沒(méi)有一個(gè)回答比較好的,甚至有將近一半的人都不知道有這個(gè)。
synchronized 同步鎖一共具有四種狀態(tài):無(wú)鎖、偏向鎖、輕量級(jí)鎖、重量級(jí)鎖,他們會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí),此過(guò)程為不可逆。所以 synchronized 鎖膨脹過(guò)程其實(shí)就是無(wú)鎖 → 偏向鎖 → 輕量級(jí)鎖 → 重量級(jí)鎖的一個(gè)過(guò)程。要理解這個(gè)過(guò)程就一定要對(duì)偏向鎖和輕量級(jí)鎖有一定的認(rèn)識(shí),如果小伙伴不熟悉則可以移步【死磕Java并發(fā)】—–深入分析synchronized的實(shí)現(xiàn)原理,這篇文章有較為詳細(xì)的說(shuō)明。下面就偏向鎖和輕量級(jí)鎖做一個(gè)簡(jiǎn)要的總結(jié)。
偏向鎖
引入偏向鎖的主要目的是:為了在無(wú)多線程競(jìng)爭(zhēng)的情況下盡量減少不必須要的輕量級(jí)鎖執(zhí)行路徑。其實(shí)在大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一個(gè)線程多次獲取,所以引入偏向鎖就可以減少很多不必要的性能開(kāi)銷(xiāo)和上下文切換。
輕量級(jí)鎖
引入輕量級(jí)鎖的主要目的是:在多線程競(jìng)爭(zhēng)不激烈的前提下,減少傳統(tǒng)的重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。需要注意的是輕量級(jí)鎖并不是取代重量級(jí)鎖,而是在大多數(shù)情況下同步塊并不會(huì)出現(xiàn)嚴(yán)重的競(jìng)爭(zhēng)情況,所以引入輕量級(jí)鎖可以減少重量級(jí)鎖對(duì)線程的阻塞帶來(lái)的開(kāi)銷(xiāo)。
所以偏向鎖是認(rèn)為環(huán)境中不存在競(jìng)爭(zhēng)情況,而輕量級(jí)鎖則是認(rèn)為環(huán)境中不存在競(jìng)爭(zhēng)或者競(jìng)爭(zhēng)不激烈,輕量級(jí)鎖所以一般都只會(huì)有少數(shù)幾個(gè)線程競(jìng)爭(zhēng)鎖對(duì)象,其他線程只需要稍微等待(自旋)下就可以獲取鎖,但是自旋次數(shù)有限制,如果超過(guò)該
下面介紹鎖膨脹過(guò)程,直接看圖:

(圖片來(lái)自:https://my.oschina.net/hosee/blog/2878328)
synchronized 用的鎖是存儲(chǔ)在 Java 對(duì)象頭里的,下圖是鎖狀態(tài)變化的情況,在分析 synchronized 鎖升級(jí)需要對(duì)照這圖:

- 一個(gè)鎖對(duì)象剛剛開(kāi)始創(chuàng)建的時(shí)候,沒(méi)有任何線程來(lái)訪問(wèn)它,它是可偏向的,它現(xiàn)在認(rèn)為只可能有一個(gè)線程來(lái)訪問(wèn)它,所以當(dāng)?shù)谝粋€(gè)線程來(lái)訪問(wèn)他的時(shí)候,它會(huì)偏向這個(gè)線程。此時(shí)線程狀態(tài)為無(wú)鎖狀態(tài),鎖標(biāo)志位為 01,此時(shí) Mark Word 如下圖:

- 當(dāng)一個(gè)線程(線程 A)來(lái)獲取鎖的時(shí),會(huì)首先檢查所標(biāo)志位,此時(shí)鎖標(biāo)志位為 01,然后檢查是否為偏向鎖,此時(shí)不為偏向鎖,所以當(dāng)前線程會(huì)修改對(duì)象頭狀態(tài)為偏向鎖,同時(shí)將對(duì)象頭中的 ThreadID 改成自己的 Thread ID,此時(shí) Mark Word 如下圖

- 如果再有一個(gè)線程(線程 B)過(guò)來(lái),此時(shí)鎖狀態(tài)為偏向鎖,該線程會(huì)檢查 Mark Word 中記錄的線程 ID 是否為自己的線程 ID,如果是,則獲取偏向鎖,執(zhí)行同步代碼塊。如果不是,則利用 CAS 嘗試替換 Mark Word 中的 Thread ID,成功,表示該線程(線程 B)獲取偏向鎖,執(zhí)行同步代碼塊,此時(shí) Mark Word 如下圖:

- 如果失敗,則表明當(dāng)前環(huán)境存在鎖競(jìng)爭(zhēng)情況,則執(zhí)行偏向鎖的撤銷(xiāo)工作(這里有一點(diǎn)需要注意的是:偏向鎖的釋放并不是主動(dòng),而是被動(dòng)的,什么意思呢?就是說(shuō)持有偏向鎖的線程執(zhí)行完同步代碼后不會(huì)主動(dòng)釋放偏向鎖,而是等待其他線程來(lái)競(jìng)爭(zhēng)才會(huì)釋放鎖)。撤銷(xiāo)偏向鎖的操作需要等到全局安全點(diǎn)才會(huì)執(zhí)行,然后暫停持有偏向鎖的線程,同時(shí)檢查該線程的狀態(tài),如果該線程不處于活動(dòng)狀態(tài)或者已經(jīng)退出同步代碼塊,則設(shè)置為無(wú)鎖狀態(tài)(線程 ID 為空,是否為偏向鎖為 0 ,鎖標(biāo)志位為01)重新偏向,同時(shí)恢復(fù)該線程。若該線程活著,則會(huì)遍歷該線程棧幀中的鎖記錄,檢查鎖記錄的使用情況,如果仍然需要持有偏向鎖,則撤銷(xiāo)偏向鎖,升級(jí)為輕量級(jí)鎖。
- 在升級(jí)為輕量級(jí)鎖之前,持有偏向鎖的線程(線程 A)是暫停的,JVM 首先會(huì)在原持有偏向鎖的線程(線程 A)的棧中創(chuàng)建一個(gè)名為鎖記錄的空間(Lock Record),用于存放鎖對(duì)象目前的 Mark Word 的拷貝,然后拷貝對(duì)象頭中的 Mark Word 到原持有偏向鎖的線程(線程 A)的鎖記錄中(官方稱(chēng)之為 Displaced Mark Word ),這時(shí)線程 A 獲取輕量級(jí)鎖,此時(shí) Mark Word 的鎖標(biāo)志位為 00,指向鎖記錄的指針指向線程 A 的鎖記錄地址,如下圖:

- 當(dāng)原持有偏向鎖的線程(線程 A)獲取輕量級(jí)鎖后,JVM 喚醒線程 A,線程 A 執(zhí)行同步代碼塊,執(zhí)行完成后,開(kāi)始輕量級(jí)鎖的釋放過(guò)程。
- 對(duì)于其他線程而言,也會(huì)在棧幀中建立鎖記錄,存儲(chǔ)鎖對(duì)象目前的 Mark Word 的拷貝。JVM 利用 CAS 操作嘗試將鎖對(duì)象的 Mark Word 更正指向當(dāng)前線程的 Lock Record,如果成功,表明競(jìng)爭(zhēng)到鎖,則執(zhí)行同步代碼塊,如果失敗,那么線程嘗試使用自旋的方式來(lái)等待持有輕量級(jí)鎖的線程釋放鎖。當(dāng)然,它不會(huì)一直自旋下去,因?yàn)樽孕倪^(guò)程也會(huì)消耗 CPU,而是自旋一定的次數(shù),如果自旋了一定次數(shù)后還是失敗,則升級(jí)為重量級(jí)鎖,阻塞所有未獲取鎖的線程,等待釋放鎖后喚醒。
- 輕量級(jí)鎖的釋放,會(huì)使用 CAS 操作將 Displaced Mark Word 替換會(huì)對(duì)象頭中,成功,則表示沒(méi)有發(fā)生競(jìng)爭(zhēng),直接釋放。如果失敗,表明鎖對(duì)象存在競(jìng)爭(zhēng)關(guān)系,這時(shí)會(huì)輕量級(jí)鎖會(huì)升級(jí)為重量級(jí)鎖,然后釋放鎖,喚醒被掛起的線程,開(kāi)始新一輪鎖競(jìng)爭(zhēng),注意這個(gè)時(shí)候的鎖是重量級(jí)鎖。
