Java?ynchronized重量級鎖的核心原理詳解
在JVM中,每個對象都關(guān)聯(lián)一個監(jiān)視器,這里的對象包含Object實例和Class實例。監(jiān)視器是一個同步工具,相當(dāng)于一個許可證,拿到許可證的線程即可進入臨界區(qū)進行操作,沒有拿到則需要阻塞等待。重量級鎖通過監(jiān)視器的方式保障了任何時間只允許一個線程通過受到監(jiān)視器保護的臨界區(qū)代碼。
1. monitor原理
jvm中每個對象都會有一個監(jiān)視器Monitor,監(jiān)視器和對象一起創(chuàng)建、銷毀。監(jiān)視器相當(dāng)于一個用來監(jiān)視這些線程進入的特殊房間,其義務(wù)是保證(同一時間)只有一個線程可以訪問被保護的臨界區(qū)代碼塊。
每一個鎖都對應(yīng)一個monitor對象,在HotSpot虛擬機中它是由ObjectMonitor實現(xiàn)的(C++實現(xiàn))
//部分屬性
ObjectMonitor() {
_count = 0; //鎖計數(shù)器
_owner = NULL;
_WaitSet = NULL; //處于wait狀態(tài)的線程,會被加入到_WaitSet
_EntryList = NULL ; //處于等待鎖block狀態(tài)的線程,會被加入到該列表
}
本質(zhì)上,監(jiān)視器是一種同步工具,也可以說是一種同步機制,主要特點是:
- 同步。監(jiān)視器所保護的臨界區(qū)代碼是互斥地執(zhí)行的。一個監(jiān)視器是一個運行許可,任一線程進入臨界區(qū)代碼都需要獲得這個許可,離開時把許可歸還。
- 協(xié)作。監(jiān)視器提供Signal機制,允許正持有許可的線程暫時放棄許可進入阻塞等待狀態(tài),等待其他線程發(fā)送Signal去喚醒;其他擁有許可的線程可以發(fā)送Signal,喚醒正在阻塞等待的線程,讓它可以重新獲得許可并啟動執(zhí)行。

每個java 對象都可以關(guān)聯(lián)一個 Monitor 對象,如果使用 synchronized 給對象上鎖(重量級)之后,該對象頭的mark word中就被設(shè)置指向 Monitor 對象的指針。
(1) 如果使用 synchronized 給obj對象上鎖,obj對象的markword就會指向一個monitor鎖對象;
(2) 剛開始 Monitor 中 Owner 為 null ;
(3) 當(dāng)Thread-2線程持有monitor對象后,就會把monitor中的owner變量設(shè)置為當(dāng)前線程Thread-2;
(4) 當(dāng)Thread-3線程想要執(zhí)行臨界區(qū)的代碼時,要判斷monitor對象的屬性O(shè)wner是否為null,如果為null,Thread-3線程就持有了對象鎖,如果不為null,Thread-3線程就會放入monitor的EntryList阻塞隊列中,處于阻塞狀態(tài)Blocked。
(5) 在 Thread-2 上鎖的過程中,如果Thread-4,Thread-5 也來執(zhí)行 synchronized(obj),也會進入EntryList BLOCKED ;
(6) Thread-2 執(zhí)行完同步代碼塊的內(nèi)容,就會釋放鎖,將owner變量置為null,并喚醒EntryList 中阻塞的線程來競爭鎖,競爭時是非公平的 ;
(7) 圖中 WaitSet 中的 Thread-0,Thread-1 是之前獲得過鎖,但條件不滿足進入 WAITING 狀態(tài)的線程,后面講 wait-notify 時會分析
2. snychronized同步代碼塊原理
public class TestSynchronized {
static final Object obj = new Object();
static int i=0;
public static void main(String[] args) {
synchronized (obj){
i++;
}
}
}
將上面的代碼反編譯為字節(jié)碼文件:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // 獲取obj對象
3: dup
4: astore_1
5: monitorenter //將obj對象的markword置為monitor指針
6: getstatic #3
9: iconst_1
10: iadd
11: putstatic #3
14: aload_1
15: monitorexit //同步代碼塊正常執(zhí)行時,將obj對象的markword重置,喚醒EntryList
16: goto 24
19: astore_2
20: aload_1
21: monitorexit //同步代碼塊出現(xiàn)異常時,將obj對象的markword重置,喚醒EntryList
22: aload_2
23: athrow
24: return
Exception table:
from to target type
6 16 19 any //監(jiān)測6-16行jvm指令,如果出現(xiàn)異常就會到第19行
19 22 19 any
這兩條指令的作用,我們直接參考JVM規(guī)范中描述:
monitorenter 指令:
每個對象有一個監(jiān)視器鎖(monitor)。當(dāng)monitor被占用時就會處于鎖定狀態(tài),線程執(zhí)行monitorenter指令時嘗試獲取monitor的所有權(quán),過程如下:
(1) 如果monitor的進入數(shù)為0,則該線程進入monitor,然后將進入數(shù)設(shè)置為1,該線程即為monitor的所有者
(2) 如果線程已經(jīng)占有該monitor,只是重新進入,則進入monitor的進入數(shù)加1.
(3) 如果其他線程已經(jīng)占用了monitor,則該線程進入阻塞狀態(tài),直到monitor的進入數(shù)為0,再重新嘗試獲取monitor的所有權(quán)。
monitorexit指令:
執(zhí)行monitorexit的線程必須是持有obj鎖對象的線程
指令執(zhí)行時,monitor的進入數(shù)減1,如果減1后進入數(shù)為0,那線程釋放monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去獲取這個 monitor 的所有權(quán)。
Synchronized的語義底層是通過一個monitor的對象來完成,其實wait/notify等方法也依賴于monitor對象,這就是為什么只有在同步的塊或者方法中才能調(diào)用wait/notify等方法,否則會拋出IllegalMonitorStateException的異常的原因。
3. synchronized同步方法原理
public class TestSynchronized {
static int i=0;
public synchronized void add(){
i++;
}
}
對應(yīng)的字節(jié)碼指令:
public synchronized void add();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field i:I
3: iconst_1
4: iadd
5: putstatic #2 // Field i:I
8: return
從反編譯的結(jié)果來看,方法的同步并沒有通過指令monitorenter和monitorexit來完成不過相對于普通方法,其常量池中多了ACC_SYNCHRONIZED標示符。JVM就是根據(jù)該標示符來實現(xiàn)方法的同步的:當(dāng)方法調(diào)用時,調(diào)用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先獲取monitor,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放monitor。在方法執(zhí)行期間,其他任何線程都無法再獲得同一個monitor對象。 其實本質(zhì)上沒有區(qū)別,只是方法的同步是一種隱式的方式來實現(xiàn),無需通過字節(jié)碼來完成。
4. 重量級鎖的開銷
處于ContentionList、EntryList、WaitSet中的線程都處于阻塞狀態(tài),線程的阻塞或者喚醒都需要操作系統(tǒng)來幫忙,Linux內(nèi)核下采用pthread_mutex_lock系統(tǒng)調(diào)用實現(xiàn),進程需要從用戶態(tài)切換到內(nèi)核態(tài)。
用戶態(tài)是應(yīng)用程序運行的空間,為了能訪問到內(nèi)核管理的資源(例如CPU、內(nèi)存、I/O),可以通過內(nèi)核態(tài)所提供的訪問接口實現(xiàn),這些接口就叫系統(tǒng)調(diào)用。
pthread_mutex_lock系統(tǒng)調(diào)用是內(nèi)核態(tài)為用戶態(tài)進程提供的Linux內(nèi)核態(tài)下互斥鎖的訪問機制,所以使用pthread_mutex_lock系統(tǒng)調(diào)用時,進程需要從用戶態(tài)切換到內(nèi)核態(tài),而這種切換是需要消耗很多時間的,有可能比用戶執(zhí)行代碼的時間還要長。
由于JVM輕量級鎖使用CAS進行自旋搶鎖,這些CAS操作都處于用戶態(tài)下,進程不存在用戶態(tài)和內(nèi)核態(tài)之間的運行切換,因此JVM輕量級鎖開銷較小。而JVM重量級鎖使用了Linux內(nèi)核態(tài)下的互斥鎖,這是重量級鎖開銷很大的原因。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
深入理解Java8新特性之Stream API的創(chuàng)建方式和中間操作步驟
Stream是Java8的一大亮點,是對容器對象功能的增強,它專注于對容器對象進行各種非常便利、高效的 聚合操作(aggregate operation)或者大批量數(shù)據(jù)操作。Stream API借助于同樣新出現(xiàn)的Lambda表達式,極大的提高編程效率和程序可讀性,感興趣的朋友快來看看吧2021-11-11
IntelliJ IDEA(或者JetBrains PyCharm)中彈出"IntelliJ IDEA License
今天小編就為大家分享一篇關(guān)于IntelliJ IDEA(或者JetBrains PyCharm)中彈出"IntelliJ IDEA License Activation"的解決辦法,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-10-10
SpringMVC訪問靜態(tài)資源的三種方式小結(jié)
這篇文章主要介紹了SpringMVC訪問靜態(tài)資源的三種方式小結(jié),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-02-02
簡單了解Java關(guān)鍵字throw和throws的區(qū)別
這篇文章主要介紹了簡單了解Java關(guān)鍵字throw和throws的區(qū)別,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-11-11
java開發(fā)ShardingSphere的路由引擎類型示例詳解
這篇文章主要為大家介紹了java開發(fā)ShardingSphere的路由引擎類型示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08
Springboot實現(xiàn)WebMvcConfigurer接口定制mvc配置詳解
這篇文章主要介紹了Springboot實現(xiàn)WebMvcConfigurer接口定制mvc配置詳解,spring?boot拋棄了傳統(tǒng)xml配置文件,通過配置類(標注@Configuration的類,@Configuration配置類相當(dāng)于一個xml配置文件)以JavaBean形式進行相關(guān)配置,需要的朋友可以參考下2023-09-09
Spring @Retryable注解輕松搞定循環(huán)重試功能
spring系列的spring-retry是另一個實用程序模塊,可以幫助我們以標準方式處理任何特定操作的重試。在spring-retry中,所有配置都是基于簡單注釋的。本文主要介紹了Spring@Retryable注解如何輕松搞定循環(huán)重試功能,有需要的朋友可以參考一下2023-04-04
解決idea?中?SpringBoot?點擊運行沒反應(yīng)按鈕成灰色的問題
在使用 Spring Boot 開發(fā)項目時,可能會遇到一個問題:點擊運行按鈕后,控制臺沒有任何輸出,項目界面也沒有顯示,這種情況可能是由多種原因?qū)е碌模疚膶⒔榻B一些常見的解決方法,需要的朋友可以參考下2023-08-08

