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