Java并發(fā)編程:Synchronized 的實(shí)現(xiàn)原理
在剛開(kāi)始學(xué)習(xí) Java 并發(fā)編程的過(guò)程中,一遇到多線程,我們就會(huì)使用 synchronized 關(guān)鍵字。在 JDK1.5 之前,Synchronized 是一個(gè)重量級(jí)鎖,效率不盡如人意。JDK1.6 對(duì) Synchronized 鎖進(jìn)行了升級(jí)優(yōu)化,引入了偏向鎖和輕量級(jí)鎖,提高了獲取鎖和釋放鎖的效率。下面我們來(lái)看一看 Synchronized 的底層實(shí)現(xiàn)原理吧。
Synchronized 的底層實(shí)現(xiàn)原理
同步原理
我們先來(lái)反編譯下面的 method1 方法:
public void method1() {
synchronized (this) {
System.out.println("This is the synchronized");
}
}
在下面,我們可以看到反編譯后的 method1 代碼:
圖片
從上面代碼執(zhí)行過(guò)程中,我們看到代碼塊同步是使用 monitorenter 和 monitorexit 指令實(shí)現(xiàn)的。
monitorenter 指令是在編譯后插入到同步代碼塊的開(kāi)始位置,monitorexit 是插入到方法結(jié)束的位置或者異常處。
JVM 要保證每個(gè) monitorenter 必須有對(duì)應(yīng)的 monitorexit 與之配對(duì)。任何對(duì)象都有一個(gè) monitor 對(duì)象與之關(guān)聯(lián),當(dāng) monitor 被對(duì)象持有后,它將處于鎖定狀態(tài)。線程執(zhí)行到 monitorenter 指令時(shí),將會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的 monitor 的所有權(quán),即嘗試獲得對(duì)象的鎖。
我們?cè)倏匆幌?Synchronized 方法同步的步驟:
public synchronized void method2() {
System.out.println("This is the synchronized method");
}
圖片
在上面編譯過(guò)的 method2() 方法的執(zhí)行過(guò)程中,Synchronized 方法的同步是使用另外一種方式實(shí)現(xiàn)的,我們可以看到它被翻譯成普通的方法調(diào)用和返回 invokevirtual、return 指令。
一個(gè)被 Synchronized 修飾的方法在 JVM 底層并沒(méi)有與之對(duì)應(yīng)的字節(jié)碼指令。當(dāng)方法調(diào)用時(shí),調(diào)用指令將會(huì)檢查方法的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志是否被設(shè)置,如果設(shè)置了執(zhí)行線程將先獲取 monitor,獲取成功之后,才能執(zhí)行方法,方法執(zhí)行完成再釋放 monitor。
Java 對(duì)象頭的概念
接下來(lái),我們?cè)賮?lái)了解一下 Java 對(duì)象頭的概念。Synchronized 用的鎖就是存放在 Java 對(duì)象頭里。如果對(duì)象是數(shù)組類(lèi)型,則虛擬機(jī)用 3 個(gè)字寬(Word)存儲(chǔ)對(duì)象頭,如果對(duì)象是非數(shù)組類(lèi)型,則用 2 字寬存儲(chǔ)對(duì)象頭。在 32 位虛擬機(jī)中,1 字寬等于 4 字節(jié),即 32bit。
我們看下面的表格,Java 對(duì)象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段)、Class Pointer(類(lèi)型指針)。Mark Word 用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),它是實(shí)現(xiàn)輕量級(jí)鎖和偏向鎖的關(guān)鍵。Class Pointer 是對(duì)象指向它的類(lèi)元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例。
圖片
Java 對(duì)象頭里的 Mark Word 里默認(rèn)存儲(chǔ)對(duì)象的 HashCode、分代年齡和鎖標(biāo)記位。我們來(lái)看一下 32 位 JVM 的 Mark Word 的默認(rèn)存儲(chǔ)結(jié)構(gòu):
圖片
在運(yùn)行期間,Mark Word 里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化。Mark Word 可能變化為存儲(chǔ)以下 4 種數(shù)據(jù),我們來(lái)看一下存儲(chǔ)結(jié)構(gòu):
圖片
在 64 位虛擬機(jī)下,Mark Word 是 64bit 大小的,我們看一下它的存儲(chǔ)結(jié)構(gòu):
圖片
我們從上面的表格中看到了引入的偏向鎖和輕量級(jí)鎖。鎖級(jí)別從低到高依次為:無(wú)鎖狀態(tài)、偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài),這 4 種狀態(tài)是會(huì)隨著競(jìng)爭(zhēng)情況而逐漸升級(jí)的。鎖可以升級(jí)但不能降級(jí),目的是提高獲得鎖和釋放鎖的效率。
下面我們來(lái)了解一下鎖升級(jí)的流程。
鎖升級(jí)
Synchronized 內(nèi)部有一個(gè)隱藏的鎖升級(jí)流程,正是因?yàn)檫@個(gè)流程的存在,使得 Synchronized 得以發(fā)揮它的高性能特性。鎖升級(jí)中最重要的 2 個(gè)升級(jí)就是偏向鎖和輕量級(jí)鎖,下面我們分別展開(kāi)討論:
偏向鎖
通常情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得,為了讓線程更容易獲得鎖而引入了偏向鎖。
所謂偏向鎖,就是當(dāng)一個(gè)線程訪問(wèn)同步代碼塊并獲取鎖時(shí),會(huì)在對(duì)象頭存儲(chǔ)鎖偏向的線程 ID。這樣,以后該線程在進(jìn)入和退出同步塊時(shí),就不需要進(jìn)行 CAS 操作來(lái)加鎖和解鎖,只需要簡(jiǎn)單地測(cè)試一下對(duì)象頭的 Mark Word 里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖。
如果測(cè)試成功,則表示線程已經(jīng)獲得了鎖;如果測(cè)試失敗,則需要再測(cè)試一下 Mark Word 中偏向鎖的標(biāo)識(shí)是否設(shè)置成 1(表示當(dāng)前是偏向鎖)。如果沒(méi)有設(shè)置,則使用 CAS 競(jìng)爭(zhēng)鎖;如果設(shè)置了,則嘗試使用 CAS 將對(duì)象頭的偏向鎖指向當(dāng)前線程。
偏向鎖在 Java6 和 Java7 是默認(rèn)啟用的,但它在應(yīng)用程序啟動(dòng)幾秒鐘之后才會(huì)被激活,我們可以配置 JVM 參數(shù)來(lái)關(guān)閉延遲:-XX:BiasedLockingStartupDelay=0。如果確定應(yīng)用程序里所有的鎖通常情況下處于競(jìng)爭(zhēng)狀態(tài),我們可以配置如下的 JVM 參數(shù)關(guān)閉偏向鎖,之后程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài):
-XX:-UseBiasedLocking=false
輕量級(jí)鎖
輕量級(jí)鎖不是用來(lái)替代傳統(tǒng)的重量級(jí)鎖的,而是在沒(méi)有多線程競(jìng)爭(zhēng)的情況下,使用輕量級(jí)鎖能夠減少性能消耗,但是當(dāng)多個(gè)線程同時(shí)競(jìng)爭(zhēng)鎖時(shí),輕量級(jí)鎖會(huì)膨脹為重量級(jí)鎖。
我們先來(lái)看一下輕量級(jí)鎖的加鎖過(guò)程。在線程在執(zhí)行同步塊之前,JVM 會(huì)先在當(dāng)前線程的棧幀中創(chuàng)建用于存儲(chǔ)鎖記錄的空間,并將對(duì)象頭中的 Mark Word 復(fù)制到鎖記錄中,官方稱為 Displaced Mark Word。然后線程嘗試使用 CAS 將對(duì)象頭中的 Mark Word 替換為指向鎖記錄的指針。如果成功,當(dāng)前線程獲
得鎖,如果失敗,表示其他線程競(jìng)爭(zhēng)鎖,當(dāng)前線程便嘗試使用自旋來(lái)獲取鎖。
輕量級(jí)解鎖時(shí),會(huì)使用原子的 CAS 操作將 Displaced Mark Word 替換回到對(duì)象頭,如果成功,則表示沒(méi)有競(jìng)爭(zhēng)發(fā)生。如果失敗,表示當(dāng)前鎖存在競(jìng)爭(zhēng),鎖就會(huì)膨脹成重量級(jí)鎖。
因?yàn)樽孕龝?huì)消耗 CPU,為了避免無(wú)用的自旋,一旦鎖升級(jí)到重量級(jí)鎖,就不會(huì)再恢復(fù)到輕量級(jí)鎖狀態(tài)。當(dāng)鎖處于這個(gè)狀態(tài)下,其他線程嘗試獲取鎖時(shí),都會(huì)被阻塞住,當(dāng)持有鎖的線程釋放鎖之后會(huì)喚醒這些線程,被喚醒的線程就會(huì)進(jìn)行新一輪的競(jìng)爭(zhēng)。
下圖是兩個(gè)線程同時(shí)爭(zhēng)奪鎖,導(dǎo)致輕量級(jí)鎖膨脹的流程。
圖片
鎖的優(yōu)缺點(diǎn)對(duì)比
對(duì)于偏向鎖、輕量級(jí)鎖和重量級(jí)鎖這三者的優(yōu)缺點(diǎn),以及適用場(chǎng)景,我們可以通過(guò)下面的表格得到一個(gè)直觀的了解:
圖片
總結(jié)
最后,我們來(lái)總結(jié)一下所講的主要內(nèi)容。首先,我們一起學(xué)習(xí)了 Synchronized 的底層實(shí)現(xiàn),Synchronized 作為一個(gè)關(guān)鍵字以它極簡(jiǎn)的語(yǔ)法也帶來(lái)了易讀性;之后,我?guī)懔私饬似蜴i的初始化、撤銷(xiāo)、關(guān)閉操作和輕量級(jí)鎖的加鎖、解鎖過(guò)程;最后,我?guī)惴治隽瞬煌i的優(yōu)缺點(diǎn)及適用場(chǎng)景,這些對(duì)你理解為什么 Synchronized 具備高性能是非常關(guān)鍵的。此外,不同鎖的,如偏向鎖、輕量級(jí)鎖對(duì)使用者來(lái)說(shuō)是透明的,這也體現(xiàn)了 Synchronized 的簡(jiǎn)單性。
隨著 JDK 的不斷發(fā)展,Synchronized 已經(jīng)做了足夠多的性能優(yōu)化。Synchronized 從一個(gè)開(kāi)銷(xiāo)很大的重量級(jí)鎖被優(yōu)化成一個(gè)可自動(dòng)適配場(chǎng)景的“智能”鎖,它可以根據(jù)場(chǎng)景轉(zhuǎn)換成偏向鎖、輕量級(jí)鎖,萬(wàn)不得已的情況下才會(huì)轉(zhuǎn)換成重量級(jí)鎖。它的應(yīng)用場(chǎng)景也隨著這些特性逐漸豐富起來(lái),在很多高并發(fā)場(chǎng)景甚至替代了 reentrantLock。