Java中的鎖升級(jí)機(jī)制:偏向鎖、輕量級(jí)鎖和重量級(jí)鎖
Monitor實(shí)現(xiàn)的鎖屬于重量級(jí)鎖,你了解過(guò)鎖升級(jí)嗎?
前面我們說(shuō)了 synchronized 底層由monitor實(shí)現(xiàn)的,它那 synchronized 到底鎖的是什么呢?隨著 JDK 版本的升級(jí),synchronized 又做出了哪些改變呢?“synchronized 性能很差”的謠言真的存在嗎?
在介紹以上內(nèi)容之前,我們要先知道重量級(jí)鎖概念。
重量級(jí)鎖
當(dāng)另外一個(gè)線程執(zhí)行到同步塊的時(shí)候,由于它沒(méi)有對(duì)應(yīng) monitor 的所有權(quán),就會(huì)被阻塞,此時(shí)控制權(quán)只能交給操作系統(tǒng),也就會(huì)從 user mode 切換到 kernel mode, 由操作系統(tǒng)來(lái)負(fù)責(zé)線程間的調(diào)度和線程的狀態(tài)變更, 這就需要頻繁的在這兩個(gè)模式下切換(上下文轉(zhuǎn)換)。有點(diǎn)競(jìng)爭(zhēng)就找內(nèi)核的行為很不好,會(huì)引起很大的開(kāi)銷(xiāo),所以大家都叫它重量級(jí)鎖,自然效率也很低,這也就給很多小伙伴留下了一個(gè)印象 —— synchronized 關(guān)鍵字相比于其他同步機(jī)制性能不好,但其實(shí)不然。
- Monitor實(shí)現(xiàn)的鎖屬于重量級(jí)鎖,里面涉及到了用戶態(tài)和內(nèi)核態(tài)的切換、進(jìn)程的上下文切換,成本較高,性能比較低。
- 在JDK 1.6引入了兩種新型鎖機(jī)制:偏向鎖和輕量級(jí)鎖,它們的引入是為了解決在沒(méi)有多線程競(jìng)爭(zhēng)或基本沒(méi)有競(jìng)爭(zhēng)的場(chǎng)景下因使用傳統(tǒng)鎖機(jī)制帶來(lái)的性能開(kāi)銷(xiāo)問(wèn)題。
一、MarkWord
在JVM虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可分為3塊區(qū)域:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充。
圖片
我們需要重點(diǎn)分析MarkWord對(duì)象頭,因?yàn)镸arkword 是保存鎖狀態(tài)的關(guān)鍵,對(duì)象鎖狀態(tài)可以從偏向鎖升級(jí)到輕量級(jí)鎖,再升級(jí)到重量級(jí)鎖,加上初始的無(wú)鎖狀態(tài),可以理解為有 4 種狀態(tài)。想在一個(gè)對(duì)象中表示這么多信息自然就要用位來(lái)存儲(chǔ)。
圖片
- hashcode:25位的對(duì)象標(biāo)識(shí)Hash碼
- age:對(duì)象分代年齡占4位
- biased_lock:偏向鎖標(biāo)識(shí),占1位 ,0表示沒(méi)有開(kāi)始偏向鎖,1表示開(kāi)啟了偏向鎖
thread:持有偏向鎖的線程ID,占23位
- epoch:偏向時(shí)間戳,占2位
- ptr_to_lock_record:輕量級(jí)鎖狀態(tài)下,指向棧中鎖記錄的指針,占30位
- ptr_to_heavyweight_monitor:重量級(jí)鎖狀態(tài)下,指向?qū)ο蟊O(jiān)視器Monitor的指針,占30位
我們可以通過(guò)lock的標(biāo)識(shí),來(lái)判斷是哪一種鎖的等級(jí)
- 后三位是001表示無(wú)鎖
- 后三位是101表示偏向鎖
- 后兩位是00表示輕量級(jí)鎖
- 后兩位是10表示重量級(jí)鎖
二、輕量級(jí)鎖
在很多的情況下,在Java程序運(yùn)行時(shí),同步塊中的代碼都是不存在競(jìng)爭(zhēng)的,不同的線程交替的執(zhí)行同步塊中的代碼。這種情況下,用重量級(jí)鎖是沒(méi)必要的。因此JVM引入了輕量級(jí)鎖的概念。
如果 CPU 通過(guò) CAS(后面會(huì)細(xì)講,戳鏈接直達(dá))就能處理好加鎖/釋放鎖,這樣就不會(huì)有上下文的切換。
但是當(dāng)競(jìng)爭(zhēng)很激烈,CAS 嘗試再多也是浪費(fèi) CPU,權(quán)衡一下,不如升級(jí)成重量級(jí)鎖,阻塞線程排隊(duì)競(jìng)爭(zhēng),也就有了輕量級(jí)鎖升級(jí)成重量級(jí)鎖的過(guò)程。
圖片
作為程序員的我們最喜歡用代碼說(shuō)話,貼心的 openjdk 官網(wǎng)提供了可以查看對(duì)象內(nèi)存布局的工具 JOL (java object layout),我們直接通過(guò) Maven 引入到項(xiàng)目中。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.14</version>
</dependency>
public class SyncSample {
private static Object LOCK = new Object();
public static void main(String[] args) {
System.out.println("----------未進(jìn)入同步塊,MarkWord 為:----------");
System.out.println(ClassLayout.parseInstance(LOCK).toPrintable());
synchronized (LOCK) {
System.out.println("----------進(jìn)入同步塊,MarkWord 為:----------");
System.out.println(ClassLayout.parseInstance(LOCK).toPrintable());
}
}
}
圖片
2.1 加鎖流程
1.在線程棧中創(chuàng)建一個(gè)Lock Record,將其obj字段指向鎖對(duì)象。
圖片
2.通過(guò)CAS指令將Lock Record的地址存儲(chǔ)在對(duì)象頭的mark word中(數(shù)據(jù)進(jìn)行交換),如果對(duì)象處于無(wú)鎖狀態(tài)則修改成功,代表該線程獲得了輕量級(jí)鎖。
圖片
3.如果是當(dāng)前線程已經(jīng)持有該鎖了,代表這是一次鎖重入。設(shè)置Lock Record第一部分為null,起到了一個(gè)重入計(jì)數(shù)器的作用。
圖片
4.如果CAS修改失敗,說(shuō)明發(fā)生了競(jìng)爭(zhēng),需要膨脹為重量級(jí)鎖。
2.2 解鎖流程
1.遍歷線程棧,找到所有obj字段等于當(dāng)前鎖對(duì)象的Lock Record。
2.如果Lock Record的Mark Word為null,代表這是一次重入,將obj設(shè)置為null后continue。
圖片
3.如果Lock Record的 Mark Word不為null,則利用CAS指令將對(duì)象頭的mark word恢復(fù)成為無(wú)鎖狀態(tài)。如果失敗則膨脹為重量級(jí)鎖。
圖片
三、偏向鎖
輕量級(jí)鎖在沒(méi)有競(jìng)爭(zhēng)時(shí)(就自己這個(gè)線程),每次重入仍然需要執(zhí)行 CAS 操作。Java 6 中引入了偏向鎖來(lái)做進(jìn)一步優(yōu)化:只有第一次使用 CAS 將線程 ID 設(shè)置到對(duì)象的 Mark Word 頭,之后發(fā)現(xiàn)這個(gè)線程 ID 是自己的就表示沒(méi)有競(jìng)爭(zhēng),不用重新 CAS。以后只要不發(fā)生競(jìng)爭(zhēng),這個(gè)對(duì)象就歸該線程所有。
圖片
可是多線程環(huán)境,也不可能只有同一個(gè)線程一直獲取這個(gè)鎖,其他線程也是要干活的,如果出現(xiàn)多個(gè)線程競(jìng)爭(zhēng)的情況,就會(huì)有偏向鎖升級(jí)的過(guò)程。
1.在線程棧中創(chuàng)建一個(gè)Lock Record,將其obj字段指向鎖對(duì)象。
圖片
2.通過(guò)CAS指令將Lock Record的線程id存儲(chǔ)在對(duì)象頭的mark word中,同時(shí)也設(shè)置偏向鎖的標(biāo)識(shí)為101,如果對(duì)象處于無(wú)鎖狀態(tài)則修改成功,代表該線程獲得了偏向鎖。
圖片
3.如果是當(dāng)前線程已經(jīng)持有該鎖了,代表這是一次鎖重入。設(shè)置Lock Record第一部分為null,起到了一個(gè)重入計(jì)數(shù)器的作用。與輕量級(jí)鎖不同的時(shí),這里不會(huì)再次進(jìn)行cas操作,只是判斷對(duì)象頭中的線程id是否是自己,因?yàn)槿鄙倭薱as操作,性能相對(duì)輕量級(jí)鎖更好一些。
圖片
思考:偏向鎖可以繞過(guò)輕量級(jí)鎖,直接升級(jí)到重量級(jí)鎖嗎?
四、面試題
面試官:Monitor實(shí)現(xiàn)的鎖屬于重量級(jí)鎖,你了解過(guò)鎖升級(jí)嗎?
Java中的synchronized有無(wú)鎖(無(wú)鎖就是沒(méi)有對(duì)資源進(jìn)行鎖定,任何線程都可以嘗試去修改它)、偏向鎖、輕量級(jí)鎖、重量級(jí)鎖四種形式,偏向鎖、輕量級(jí)鎖、重量級(jí)鎖分別對(duì)應(yīng)了鎖只被一個(gè)線程持有、不同線程交替持有鎖、多線程競(jìng)爭(zhēng)鎖三種情況
鎖別 | 描述 |
重量級(jí)鎖 | 底層使用的Monitor實(shí)現(xiàn),里面涉及到了用戶態(tài)和內(nèi)核態(tài)的切換、進(jìn)程的上下文切換,成本較高,性能比較低。 |
輕量級(jí)鎖 | 線程加鎖的時(shí)間是錯(cuò)開(kāi)的(也就是沒(méi)有競(jìng)爭(zhēng)),可以使用輕量級(jí)鎖來(lái)優(yōu)化。輕量級(jí)修改了對(duì)象頭的鎖標(biāo)志,相對(duì)重量級(jí)鎖性能提升很多。每次修改都是CAS操作,保證原子性 |
偏向鎖 | 一段很長(zhǎng)的時(shí)間內(nèi)都只被一個(gè)線程使用鎖,可以使用了偏向鎖,在第一次獲得鎖時(shí),會(huì)有一個(gè)CAS操作,之后該線程再獲取鎖,只需要判斷mark word中是否是自己的線程id即可,而不是開(kāi)銷(xiāo)相對(duì)較大的CAS命令 |