新聞中心
一、前言
在面試題中經(jīng)常會(huì)有這么一道面試題,談一下synchronized鎖升級過程?

之前背了一些,很多文章也說了,到底怎么什么條件才會(huì)觸發(fā)升級,一直不太明白。
實(shí)踐是檢驗(yàn)真理的唯一標(biāo)準(zhǔn),今天就和大家一起實(shí)踐一下,什么條件才會(huì)升級!
二、為什么會(huì)有鎖升級過程?
在實(shí)踐之前,我們先一步步的來了解!為什么要升級呢?
在JDK1.6之前,synchronized的性能一直沒有ReentrantLock性能高,主要是因?yàn)閟ynchronized涉及到用戶態(tài)和內(nèi)核態(tài)的切換,這個(gè)是在操作系統(tǒng)和硬件是非常消耗資源的。
經(jīng)過不斷的統(tǒng)計(jì)分析,發(fā)現(xiàn)大部分時(shí)間一個(gè)鎖都是一個(gè)線程去獲取,如果只有一個(gè)線程來嘗試加鎖,就是重量級鎖,顯而浪費(fèi)資源。
「總之,鎖的升級過程是為了提高多線程環(huán)境下的性能和吞吐量,減少同步操作的開銷,并盡量避免線程切換的開銷。Java虛擬機(jī)根據(jù)線程競爭的情況和鎖的使用情況自動(dòng)進(jìn)行鎖的升級和降級,以優(yōu)化多線程程序的性能?!?/p>
此時(shí),就引入了很多鎖類型,下面我們來具體看看!
三、鎖分類
偏向鎖:偏向鎖是為了解決單線程訪問的場景,偏向鎖允許第一個(gè)訪問共享資源的線程獲得鎖,把線程id存到對象頭中,后續(xù)的訪問可以直接獲得鎖,而不需要競爭。
輕量級鎖:當(dāng)一個(gè)或多個(gè)線程嘗試獲取同一個(gè)鎖時(shí),偏向鎖會(huì)升級為輕量級鎖。輕量級鎖采用CAS(Compare and Swap)操作來減小鎖的競爭。采用自適應(yīng)自旋!
重量級鎖:操作系統(tǒng)的調(diào)度器會(huì)介入,將競爭鎖的線程掛起,直到鎖被釋放為止,重量級鎖的開銷相對較高。
「補(bǔ)充:」
「自適應(yīng)自旋的基本思想是根據(jù)鎖的爭用情況,決定線程是否應(yīng)該自旋等待,以及自旋等待的時(shí)間,一般情況為自旋10次。」
四、對象內(nèi)存結(jié)構(gòu)
我們在說鎖的升級過程之前,需要了解一下對象的內(nèi)存結(jié)構(gòu),因?yàn)樵阪i升級過程中會(huì)往對象頭上進(jìn)行填充信息!一個(gè)對象分為:對象頭、實(shí)例數(shù)據(jù)、對其填充位三部分組成。
我們本次主要用到對象頭,我們再看一下詳細(xì)的對象頭信息里有什么:
五、圖解鎖升級過程
先來一個(gè)簡圖:
下面引用百度上的一張?jiān)敿?xì)一點(diǎn)的圖:
我們來詳細(xì)的說一下鎖的升級過程,在每一個(gè)鎖切換時(shí)的條件是什么?
在JDK8時(shí),偏向鎖默認(rèn)是在程序啟動(dòng)后4s自動(dòng)開啟的,在JKD15之后默認(rèn)是不開啟的!
可以設(shè)置無延遲時(shí)間啟動(dòng):-XX:BiasedLockingStartupDelay=0也可以不啟動(dòng)偏向鎖:-XX:-UseBiasedLocking = false。
直接說有點(diǎn)不形象,我們下面結(jié)合代碼來實(shí)戰(zhàn),看一下具體情況!
六、實(shí)戰(zhàn)鎖升級過程
為了我們能夠查詢對象結(jié)構(gòu),我們需要引入jar幫助我們查看!
1、導(dǎo)入依賴
「注意」:不要使用高版本的,高版本不顯示2進(jìn)制,不好觀察!
org.openjdk.jol
jol-core
0.10
2、實(shí)戰(zhàn)代碼和解析
我們來從序號1開始,上面也說了默認(rèn)4s后開啟偏向鎖,我們會(huì)發(fā)現(xiàn)序號1打印的對象頭序號為:001我們的對象大小為20,內(nèi)部幫我們補(bǔ)位來滿足是8的倍數(shù)。方便操作系統(tǒng)進(jìn)行尋址,不會(huì)有碎片組合!這個(gè)大家可以詳細(xì)搜一下,這里就一帶而過了哈!
此時(shí)我們睡眠6s,包裝偏向鎖開啟成功!
我們來到序號2,開啟了偏向鎖,我們發(fā)現(xiàn)對象頭序號為:101。
「節(jié)點(diǎn):從無鎖到偏向鎖切換的條件:JDK8中默認(rèn)4s后開啟,JDK15需要手動(dòng)開啟」。
來到序號3和4一起說吧,當(dāng)我們進(jìn)行synchronized加鎖時(shí),對象的頭信息中會(huì)記錄上當(dāng)前線程的id,下面再有加鎖的,直接判斷線程id是否一致,一致直接進(jìn)入代碼塊。不一致后面再說!我們發(fā)現(xiàn)在序號4時(shí),已經(jīng)出了代碼塊,在此查詢加鎖的對象,信息依舊在,不會(huì)進(jìn)行移除,這就是偏向,直到下一個(gè)線程把上一個(gè)替換掉!
代碼里循環(huán)了三次,對象都是一樣的!
「節(jié)點(diǎn):在只有一個(gè)線程訪問代碼塊的時(shí)候,對象中會(huì)記錄當(dāng)前線程id?!?/p>
「以上都是在一個(gè)線程來訪問的情況下」
來到序號5,我們新建了一個(gè)線程來進(jìn)行加鎖。此時(shí)會(huì)判斷當(dāng)前線程id和新線程id是否一致,不一致就會(huì)認(rèn)為有競爭關(guān)系,會(huì)立刻切換為輕量級鎖。對象頭序號為:00
「節(jié)點(diǎn):當(dāng)有兩個(gè)線程交替獲取鎖時(shí),不存在同時(shí)競爭獲取鎖時(shí)?!?/p>
序號6和7一起說,我們讓上面序號5這個(gè)線程獲取鎖后睡眠3s,持續(xù)獲得鎖。在開啟一個(gè)新的線程去競爭獲取鎖,此時(shí)先進(jìn)行自適應(yīng)CAS自旋,一般10次后一直沒辦法獲取鎖,判定為激烈競爭關(guān)系。變?yōu)橹亓考夋i,序號7線程會(huì)進(jìn)行放到阻塞隊(duì)列中。對象頭序號為:10。
經(jīng)過睡眠后,序號6在此獲取對象的信息時(shí),已經(jīng)變?yōu)橹亓考夋i!
「節(jié)點(diǎn):有兩個(gè)及其以上線程同時(shí)獲取鎖,且在自適應(yīng)自旋范圍內(nèi)沒有獲取到鎖」。
下面是代碼,大家可以在本地試一下!
/**
* jvm默認(rèn)延時(shí)4s自動(dòng)開啟偏向鎖,
* 可通過 -XX:BiasedLockingStartupDelay=0
* 取消延時(shí)如果不要偏向鎖,可通過-XX:-UseBiasedLocking = false
* @author wangzhenjun
* @date 2023/10/18 14:42
*/
public class LockUp {
@SneakyThrows
public static void main(String[] args) {
LockInfo lockInfo = new LockInfo();
System.out.println("1.無狀態(tài):" + ClassLayout.parseInstance(lockInfo).toPrintable());
Thread.sleep(6000);
LockInfo lock = new LockInfo();
System.out.println("2.已經(jīng)開啟了偏向鎖模式:" + ClassLayout.parseInstance(lock).toPrintable());
for (int i = 0; i < 3; i++) {
synchronized (lock) {
System.out.println("3.偏向鎖模式下,加鎖狀態(tài):" + ClassLayout.parseInstance(lock).toPrintable());
}
System.out.println("4.鎖釋放了,加鎖狀態(tài):" + ClassLayout.parseInstance(lock).toPrintable());
}
new Thread(() -> {
synchronized (lock) {
System.out.println("5.輕量級鎖,加鎖狀態(tài):" + ClassLayout.parseInstance(lock).toPrintable());
System.out.println("睡眠3s");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("6.輕量級鎖=>重量級鎖,加鎖狀態(tài):" + ClassLayout.parseInstance(lock).toPrintable());
}
}).start();
Thread.sleep(1000);
new Thread(() -> {
synchronized (lock) {
System.out.println("重量級鎖,加鎖狀態(tài):" + ClassLayout.parseInstance(lock).toPrintable());
}
}).start();
}
}
七、總結(jié)與拓展
經(jīng)過實(shí)戰(zhàn),我們知道了每一個(gè)的切換條件,可以在面試中好好地回答了。不至于面試官反問一下就不堅(jiān)定了!
關(guān)于切換到重量級鎖后,有興趣的話,可以下載openJDK源碼去看一下關(guān)于hotspot/src/share/vm/runtime/objectMonitor.cpp和hotspot/src/share/vm/runtime/objectMonitor.hpp。
源碼下載地址:https://github.com/openjdk/jdk8
objectMonitor.cpp:是 OpenJDK 中實(shí)現(xiàn) Java 同步機(jī)制的核心部分,它負(fù)責(zé)管理對象監(jiān)視器,確保多線程程序能夠正確協(xié)同工作,實(shí)現(xiàn)線程同步和等待/通知機(jī)制。
objectMonitor.hpp:主要用于定義對象監(jiān)視器的接口和數(shù)據(jù)結(jié)構(gòu),為實(shí)際的對象監(jiān)視器的實(shí)現(xiàn)提供了基礎(chǔ)。
網(wǎng)站欄目:不吃飯也要掌握的Synchronized鎖升級過程
URL標(biāo)題:http://m.fisionsoft.com.cn/article/codpohi.html


咨詢
建站咨詢
