Java?ynchronized重量級鎖的核心原理詳解
在JVM中,每個對象都關(guān)聯(lián)一個監(jiān)視器,這里的對象包含Object實例和Class實例。監(jiān)視器是一個同步工具,相當(dāng)于一個許可證,拿到許可證的線程即可進(jìn)入臨界區(qū)進(jìn)行操作,沒有拿到則需要阻塞等待。重量級鎖通過監(jiān)視器的方式保障了任何時間只允許一個線程通過受到監(jiān)視器保護(hù)的臨界區(qū)代碼。
1. monitor原理
jvm中每個對象都會有一個監(jiān)視器Monitor,監(jiān)視器和對象一起創(chuàng)建、銷毀。監(jiān)視器相當(dāng)于一個用來監(jiān)視這些線程進(jìn)入的特殊房間,其義務(wù)是保證(同一時間)只有一個線程可以訪問被保護(hù)的臨界區(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)視器所保護(hù)的臨界區(qū)代碼是互斥地執(zhí)行的。一個監(jiān)視器是一個運行許可,任一線程進(jìn)入臨界區(qū)代碼都需要獲得這個許可,離開時把許可歸還。
- 協(xié)作。監(jiān)視器提供Signal機制,允許正持有許可的線程暫時放棄許可進(jìn)入阻塞等待狀態(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),也會進(jìn)入EntryList
BLOCKED
;
(6) Thread-2 執(zhí)行完同步代碼塊的內(nèi)容,就會釋放鎖,將owner變量置為null,并喚醒EntryList
中阻塞的線程來競爭鎖,競爭時是非公平的 ;
(7) 圖中 WaitSet 中的 Thread-0,Thread-1 是之前獲得過鎖,但條件不滿足進(jìn)入 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的進(jìn)入數(shù)為0,則該線程進(jìn)入monitor,然后將進(jìn)入數(shù)設(shè)置為1,該線程即為monitor的所有者
(2) 如果線程已經(jīng)占有該monitor,只是重新進(jìn)入,則進(jìn)入monitor的進(jìn)入數(shù)加1.
(3) 如果其他線程已經(jīng)占用了monitor,則該線程進(jìn)入阻塞狀態(tài),直到monitor的進(jìn)入數(shù)為0,再重新嘗試獲取monitor的所有權(quán)。
monitorexit指令:
執(zhí)行monitorexit的線程必須是持有obj鎖對象的線程
指令執(zhí)行時,monitor的進(jìn)入數(shù)減1,如果減1后進(jìn)入數(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
標(biāo)示符。JVM就是根據(jù)該標(biāo)示符來實現(xiàn)方法的同步的:當(dāng)方法調(diào)用時,調(diào)用指令將會檢查方法的 ACC_SYNCHRONIZED
訪問標(biāo)志是否被設(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),進(jì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)進(jìn)程提供的Linux內(nèi)核態(tài)下互斥鎖的訪問機制,所以使用pthread_mutex_lock系統(tǒng)調(diào)用時,進(jìn)程需要從用戶態(tài)切換到內(nèi)核態(tài),而這種切換是需要消耗很多時間的,有可能比用戶執(zhí)行代碼的時間還要長。
由于JVM輕量級鎖使用CAS進(jìn)行自旋搶鎖,這些CAS操作都處于用戶態(tài)下,進(jìn)程不存在用戶態(tài)和內(nèi)核態(tài)之間的運行切換,因此JVM輕量級鎖開銷較小。而JVM重量級鎖使用了Linux內(nèi)核態(tài)下的互斥鎖,這是重量級鎖開銷很大的原因。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
java 下執(zhí)行mysql 批量插入的幾種方法及用時
java 下執(zhí)行mysql 批量插入的幾種方法及用時,1000次插入方法的比較。2013-04-04解決IDEA克隆代碼后在右下角沒有g(shù)it分支的問題
這篇文章主要介紹了解決IDEA克隆代碼后在右下角沒有g(shù)it分支的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02JAVA中通過自定義注解進(jìn)行數(shù)據(jù)驗證的方法
java 自定義注解驗證可自己添加所需要的注解,下面這篇文章主要給大家介紹了關(guān)于JAVA中通過自定義注解進(jìn)行數(shù)據(jù)驗證的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-08-08Spring?Cloud微服務(wù)架構(gòu)Sentinel數(shù)據(jù)雙向同步
這篇文章主要為大家介紹了Spring?Cloud微服務(wù)架構(gòu)Sentinel數(shù)據(jù)雙向同步示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10Spring Data JPA 設(shè)置字段默認(rèn)值方式
這篇文章主要介紹了Spring Data JPA設(shè)置字段默認(rèn)值方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11Mybatis注解方式完成輸入?yún)?shù)為list的SQL語句拼接方式
這篇文章主要介紹了Mybatis注解方式完成輸入?yún)?shù)為list的SQL語句拼接方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11