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