Java synchronized最細講解
前言
線程安全問題的主要誘因有兩點,一是存在共享數(shù)據(jù)(也稱臨界資源),二是存在多條線程共同操作共享數(shù)據(jù)。
因此為了解決這個問題,我們可能需要這樣一個方案,當存在多個線程操作共享數(shù)據(jù)時,需要保證同一時刻有且只有一個線程在操作共享數(shù)據(jù),其他線程必須等到該線程處理完數(shù)據(jù)后再進行,這種方式有個高尚的名稱叫互斥鎖,即能達到互斥訪問目的的鎖,也就是說當一個共享數(shù)據(jù)被當前正在訪問的線程加上互斥鎖后,在同一個時刻,其他線程只能處于等待的狀態(tài),直到當前線程處理完畢釋放該鎖。
在 Java 中,關(guān)鍵字 synchronized可以保證在同一個時刻,只有一個線程可以執(zhí)行某個方法或者某個代碼塊(主要是對方法或者代碼塊中存在共享數(shù)據(jù)的操作),同時我們還應該注意到synchronized另外一個重要的作用,synchronized可保證一個線程的變化(主要是共享數(shù)據(jù)的變化)被其他線程所看到(保證可見性,完全可以替代Volatile功能),這點確實也是很重要的。
synchronized三種作用范圍(給對象加鎖)
在靜態(tài)方法上加鎖;
在非靜態(tài)方法上加鎖;
在代碼塊上加鎖;
public class SynchronizedSample { private final Object lock = new Object(); private static int money = 0; //非靜態(tài)方法 public synchronized void noStaticMethod(){ money++; } //靜態(tài)方法 public static synchronized void staticMethod(){ money++; } public void codeBlock(){ //代碼塊 synchronized (lock){ money++; } } }
作用范圍 | 鎖對象 |
---|---|
非靜態(tài)方法 | 當前對象 => this |
靜態(tài)方法 | 類對象 => SynchronizedSample.class (一切皆對象,這個是類對象) |
代碼塊 | 指定對象 => lock (以上面的代碼為例) |
Synchronization實現(xiàn)原理
先理解Java對象頭與Monitor
1.對象頭:鎖的類型和狀態(tài)和對象頭的Mark Word息息相關(guān);
對象頭分為二個部分,Mard Word 和 Klass Word
對象頭結(jié)構(gòu) | 存儲信息-說明 |
---|---|
Mard Word | 存儲對象的hashCode、鎖信息或分代年齡或GC標志等信息 |
Klass Word | 存儲指向?qū)ο笏鶎兕悾ㄔ獢?shù)據(jù))的指針,JVM通過這個確定這個對象屬于哪個類 |
其中Mark Word在默認情況下存儲著對象的HashCode、分代年齡、鎖標記位等以下是32位JVM的Mark Word默認存儲結(jié)構(gòu)
鎖狀態(tài) | 25bit | 4bit | 1bit是否是偏向鎖 | 2bit 鎖標志位 |
---|---|---|---|---|
無鎖狀態(tài) | 對象HashCode | 對象分代年齡 | 0 | 01 |
主要分析一下重量級鎖也就是通常說synchronized的對象鎖,鎖標識位為10,其中指針指向的是monitor對象(也稱為管程或監(jiān)視器鎖)的起始地址。每個對象都存在著一個 monitor 與之關(guān)聯(lián),對象與其 monitor 之間的關(guān)系有存在多種實現(xiàn)方式,如monitor可以與對象一起創(chuàng)建銷毀或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有后,它便處于鎖定狀態(tài)。在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下(位于HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現(xiàn)的)
//👇圖詳細介紹重要變量的作用 ObjectMonitor() { _header = NULL; _count = 0; // 重入次數(shù) _waiters = 0, // 等待線程數(shù) _recursions = 0; _object = NULL; _owner = NULL; // 當前持有鎖的線程 _WaitSet = NULL; // 調(diào)用了 wait 方法的線程被阻塞 放置在這里 _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 等待鎖 處于block的線程 有資格成為候選資源的線程 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
ObjectMonitor中有兩個隊列,_WaitSet 和 _EntryList,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 集合,當線程獲取到對象的monitor 后進入 _Owner 區(qū)域并把monitor中的owner變量設置為當前線程同時monitor中的計數(shù)器count加1,若線程調(diào)用 wait() 方法,將釋放當前持有的monitor,owner變量恢復為null,count自減1,同時該線程進入 WaitSet集合中等待被喚醒。若當前線程執(zhí)行完畢也將釋放monitor(鎖)并復位變量的值,以便其他線程進入獲取monitor(鎖)。如下圖所示
由此看來,monitor對象存在于每個Java對象的對象頭中(存儲的指針的指向),synchronized鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因,同時也是notify/notifyAll/wait等方法存在于頂級對象Object中的原因(關(guān)于這點稍后還會進行分析)。
jdk6 之后做了改進,引入了偏向鎖和輕量級鎖:
- 依賴底層操作系統(tǒng)的 mutex 相關(guān)指令實現(xiàn),加鎖解鎖需要在用戶態(tài)和內(nèi)核態(tài)之間切換,性能損耗非常明顯。
- 研究人員發(fā)現(xiàn),大多數(shù)對象的加鎖和解鎖都是在特定的線程中完成。也就是出現(xiàn)線程競爭鎖的情況概率比較低。他們做了一個實驗,找了一些典型的軟件,測試同一個線程加鎖解鎖的重復率,如下圖所示,可以看到重復加鎖比例非常高。早期JVM 有 19% 的執(zhí)行時間浪費在鎖上。
1.無鎖到偏向鎖轉(zhuǎn)化的過程
- 首先A 線程訪問同步代碼塊,使用CAS 操作將 Thread ID 放到 Mark Word 當中;
- 如果CAS 成功,此時線程A 就獲取了鎖
- 如果線程CAS 失敗,證明有別的線程持有鎖,例如上圖的線程B 來CAS 就失敗的,這個時候啟動偏向鎖撤銷 (revoke bias);
- 鎖撤銷流程:
- 讓 A線程在全局安全點阻塞(類似于GC前線程在安全點阻塞)
- 遍歷線程棧,查看是否有被鎖對象的鎖記錄( Lock Record),如果有Lock Record,需要修復鎖記錄和Markword,使其變成無鎖狀態(tài)。
- 恢復A線程
- 將是否為偏向鎖狀態(tài)置為 0 ,開始進行輕量級加鎖流程
2.偏向鎖升級輕量級
- 線程在自己的棧楨中創(chuàng)建鎖記錄 LockRecord。
- 線程A 將 Mark Word 拷貝到線程棧的 Lock Record中
- 將鎖記錄中的Owner指針指向加鎖的對象(存放對象地址)
- 將鎖對象的對象頭的MarkWord替換為指向鎖記錄的指針。
- 這時鎖標志位變成 00 ,表示輕量級鎖
其實就是撤銷偏向鎖后,當前線程棧中會分配鎖記錄,并拷貝Mark Word到鎖記錄中。然后兩個線程用CAS的方式去修改Mark Word中的指針指向自己,假如說第一個線程修改成功了,然后將鎖升級為輕量級鎖,去執(zhí)行同步語句塊中的內(nèi)容。
3.輕量級到重量級
修改失敗的第二個線程會進入自旋狀態(tài),自旋結(jié)束后會繼續(xù)去嘗試CAS修改指針指向自己。如果自旋失敗超過一定次數(shù)的時候(這個次數(shù)會動態(tài)進行調(diào)整),會請求JVM將此時的鎖狀態(tài)升級為重量級鎖,這是依賴于底層操作系統(tǒng)的調(diào)度庫來實現(xiàn)的。接著將Mark Word指向重量級鎖Monitor的指針,然后掛起當前第二個線程(被放在Monitor的_EntryList中)。等一個線程執(zhí)行完畢后,會查看當前Mark Word中的指針是否仍然指向自己,如果是自己的話就釋放鎖,否則不是自己的話,說明此時已經(jīng)升級成了重量級鎖,除了釋放鎖之后,還會喚醒阻塞的線程,進行新一輪的鎖競爭。在此之后,該鎖就一直會是重量級鎖存在了
ps:為什么設計自旋數(shù)超過一定限制設置為重量級鎖?
一般來說,同步代碼塊內(nèi)的代碼應該很快就執(zhí)行結(jié)束,這時候修改失敗的第二個線程自旋一段時間是很容易拿到鎖的,但是如果不巧,沒拿到,自旋其實就是死循環(huán),很耗CPU的,因此就直接轉(zhuǎn)成重量級鎖咯,這樣就不用了線程一直自旋了。
源碼才學疏淺只了解到:
synchronized 在代碼塊上是通過 monitorenter 和 monitorexit指令實現(xiàn),在靜態(tài)方法和 方法上加鎖是在方法的flags 中加入 ACC_SYNCHRONIZED 。JVM 運行方法時檢查方法的flags,遇到同步標識開始啟動前面的加鎖流程,在方法內(nèi)部遇到monitorenter指令開始加鎖。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
SpringBoot之RestTemplate在URL中轉(zhuǎn)義字符的問題
這篇文章主要介紹了SpringBoot之RestTemplate在URL中轉(zhuǎn)義字符的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-06-06Mybatis如何根據(jù)List批量查詢List結(jié)果
這篇文章主要介紹了Mybatis如何根據(jù)List批量查詢List結(jié)果,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03Spring中@ControllerAdvice注解的用法解析
這篇文章主要介紹了Spring中@ControllerAdvice注解的用法解析,顧名思義,@ControllerAdvice就是@Controller 的增強版,@ControllerAdvice主要用來處理全局數(shù)據(jù),一般搭配@ExceptionHandler、@ModelAttribute以及@InitBinder使用,需要的朋友可以參考下2023-10-10Spring注解@Value在controller無法獲取到值的解決
這篇文章主要介紹了Spring注解@Value在controller無法獲取到值的解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11