1. 程式人生 > >Java基礎之Synchronized原理

Java基礎之Synchronized原理

![](https://img2020.cnblogs.com/blog/686418/202006/686418-20200630151944391-1320178406.png) 思維導圖svg: https://note.youdao.com/ynoteshare1/index.html?id=eb05fdceddd07759b8b82c5b9094021a&type=note 在多執行緒使用共享資源的時候, 我們可以使用synchronized來鎖定共享資源,使得同一時刻,只有一個執行緒可以訪問和修改它,修改完畢後,其他執行緒才可以使用。這種方式叫做互斥鎖。 當一個共享資料被當前正在訪問到執行緒添加了互斥鎖之後,在同一時刻,其他執行緒只能等待,直到當前執行緒釋放該鎖。 synchronized可以新增互斥鎖,並且保證被其他執行緒看到。 ## synchronized的三種應用方式 synchronized關鍵字最主要有以下3種應用方式,下面分別介紹 - 修飾例項方法,作用於當前例項加鎖,進入同步程式碼錢要獲得當前例項的鎖 - 修飾靜態方法,作用於當前類物件加鎖,進入同步程式碼前要獲得當前類物件的鎖 - 修飾程式碼塊,指定加鎖物件,對給定物件加鎖,進入同步程式碼塊前要獲得給定物件的鎖 ### synchronized作用於例項方法 我們設定類變數`static`為共享資源, 然後多個執行緒去修改。修改的含義是: 先讀取,計算,再寫入。那麼這個過程就不是原子的,多個執行緒操作就會出現共享資源爭搶問題。 我們在例項方法上新增synchronized,那麼,同一個例項執行本方法時,搶到鎖到可以執行。 ```java public class AccountingSync implements Runnable{ //共享資源(臨界資源) static int i=0; /** * synchronized 修飾例項方法 */ public synchronized void increase(){ i++; } @Override public void run() { for(int j=0;j<1000000;j++){ increase(); } } public static void main(String[] args) throws InterruptedException { AccountingSync instance=new AccountingSync(); Thread t1=new Thread(instance); Thread t2=new Thread(instance); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } /** * 輸出結果: * 2000000 */ } ``` 上述程式碼中,開啟兩個執行緒去操作共享變數,兩個執行緒執行的是同一個例項物件。如果不新增synchronized,其中`i++`不是原子操作,該操作先讀取值,然後再寫入一個新值。如果兩個執行緒都讀取了i=5,然後執行緒1寫入i=6.執行緒2後寫入,但也是寫入i=6, 並不是我們期望的i=7. 新增synchronized修飾後,執行緒安全,執行緒必須獲取到這個實力到鎖才能執行讀取和寫入。 注意,我們synchronized修飾到是類方法,鎖的是例項,當多個執行緒操作不同例項時,會使用不同例項的鎖,就無法保證修改static變數的有序性了。 ```java public class AccountingSyncBad implements Runnable{ static int i=0; public synchronized void increase(){ i++; } @Override public void run() { for(int j=0;j<1000000;j++){ increase(); } } public static void main(String[] args) throws InterruptedException { //new新例項 Thread t1=new Thread(new AccountingSyncBad()); //new新例項 Thread t2=new Thread(new AccountingSyncBad()); t1.start(); t2.start(); //join含義:當前執行緒A等待thread執行緒終止之後才能從thread.join()返回 t1.join(); t2.join(); System.out.println(i); } } ``` 上述程式碼,兩個執行緒持有不同的物件instance,也就是使用不同的鎖, 也就不會互斥訪問共享資源,就會出現執行緒安全問題。 ### synchronized作用於靜態方法 synchronized作用於靜態方法時,鎖就是當前類到class物件鎖。由於靜態成員變數不專屬於任何一個例項物件,是類成員,因此通過class物件鎖可以控制靜態成員的併發操作。 ### synchronized同步程式碼塊 除了使用關鍵字修飾例項方法和靜態方法外,還可以使用同步程式碼塊。 ```java public class AccountingSync implements Runnable{ static AccountingSync instance=new AccountingSync(); static int i=0; @Override public void run() { //省略其他耗時操作.... //使用同步程式碼塊對變數i進行同步操作,鎖物件為instance synchronized(instance){ for(int j=0;j<1000000;j++){ i++; } } } public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(instance); Thread t2=new Thread(instance); t1.start();t2.start(); t1.join();t2.join(); System.out.println(i); } } ``` 上述程式碼,將synchronized作用於一個給定的例項物件instance, 即當前例項物件就是鎖物件,每次當執行緒進入synchronized包裹到程式碼塊時,就會要求當前執行緒持有instance例項物件鎖,如果當前有其他執行緒正持有該物件鎖,那麼新到到執行緒就必須等待,這樣也就保證了每次只有一個執行緒執行`i++`操作。當然, 還可以使用this或者class ```java //this,當前例項物件鎖 synchronized(this){ for(int j=0;j<1000000;j++){ i++; } } //class物件鎖 synchronized(AccountingSync.class){ for(int j=0;j<1000000;j++){ i++; } } ``` 瞭解完synchronized到基本含義和使用方式後,我們進一步深入理解synchronized的底層實現原理。 ## synchronized底層語義原理 Java虛擬機器中的同步(Synchronization)基於進入和退出管程(Monitor)物件實現,無論是顯示同步(有明確的monitorenter和monitorexit指令,即同步程式碼塊)還是隱式同步都是如此。在Java語言中,同步用的最多到地方可能是被synchronized修飾的同步方法。同步方法並不是由monitorenter和monitorexit指令來實現同步到,而是由方法呼叫指令讀取執行時常量池中方法到ACC_SYNCHRONIZED標誌來隱式實現的,關於這點,稍後分析。下面先來了解一個概念:Java物件頭,這對深入理解synchronized實現原理非常關鍵。 ### 理解Java物件頭與Monitor 在JVM中,物件在記憶體中到佈局分為三塊區域:物件頭,例項資料和對齊填充。 如下: ![](https://img-blog.csdn.net/20170603163237166?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamF2YXplamlhbg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) - 例項變數: 存放類的屬性資料資訊,包括父類的屬性資訊,如果是陣列的例項部分還包括陣列的長度,這部分記憶體按4位元組對齊。 - 填充資料:由於虛擬機器要求物件起始地址必須是8位元組的整數倍。填充資料不是必須存在的,僅僅是為了位元組對齊。 而對於頂部,則是Java頭物件,它是實現synchronized的鎖物件的基礎,這點我們重點分析它。 一般而言,synchronized使用的鎖物件是儲存在Java物件頭裡的,jvm採用2個字來儲存物件頭(如果物件是陣列則會分配3個字,多出來到1個字記錄的是陣列長度),其主要結構是由Mark Word和Class Metadata Address組成,其結構說明如下: | 虛擬機器位數| 頭物件結構 | 說明| |----|----|---| |32/64bit | Mark Word | 儲存物件的hashcode, 鎖資訊或分代年齡或GC標誌等資訊 | |32/64bit | Class Metadata Address | 型別指標指向物件的類元資料, JVM通過這個指標確定該物件是哪個類的例項 | 其中Mark Word在預設情況下儲存著物件的HashCode, 分代年齡,鎖標記等, 以下是32位JVM的Mark Word預設儲存結構。 | 鎖狀態 | 25bit | 4bit | 1bit是否是偏向鎖 | 2bit鎖標誌位| |-----| -----------|------| ------------------| -------------| | 無鎖狀態| 物件HashCode | 物件分代年齡 | 0 | 01 | 由於物件頭的資訊是與物件自身定義的資料沒有關係到額外儲存成本,因此考慮到JVM的空間效率,Mark Word被設計成為一個非固定的資料結構,以便儲存更多有效的資料,它會根據物件本身的狀態複用自己的儲存空間,如32位JVM下,除了上述列出的Mark Word預設儲存結構外,還有如下可能變化的結構: ![](https://img-blog.csdn.net/20170603172215966?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamF2YXplamlhbg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 其中,輕量級鎖和偏向鎖是Java 6對synchronized鎖進行優化後新增加的,我們稍後簡要分析。這裡我們主要分析一下重量級鎖也就是通常說的synchronized的物件鎖,鎖標識位10,其中指標指向的時monitor物件(也稱為管程或監視器鎖)的起始地址。每個物件都存在著一個monitor與之關聯,物件與其monitor之間的關係有存在多種實現方式,如monitor可以與物件一起建立銷燬或當執行緒試圖獲取物件鎖時自動生成,但當一個monitor被某個執行緒持有後,它便處於鎖定狀態。在Java虛擬機器(HotSpot)中,monitor是由ObjetMonitor實現的,其主要資料結構如下(位於HotSpot虛擬機器原始碼ObjectMonitor.hpp檔案,C++實現的) ``` ObjectMonitor() { _header = NULL; _count = 0; //記錄個數 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; //處於wait狀態的執行緒,會被加入到_WaitSet _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`區域, 並把monitor中到onwer變數設定為當前執行緒, 同時monitor中的計數器count+1。 若執行緒呼叫wait()方法,將釋放當前持有的monitor, owner=null, count-1, 同時該執行緒進入waitSet集合中等待被喚醒。若當前執行緒執行完畢也將釋放monitor(鎖),並復位變數的值,以便其他執行緒進入獲取monitor(鎖)。 如下圖所示: ![](https://img-blog.csdn.net/20170604114223462?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamF2YXplamlhbg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 由此看來,monitor物件存在於每個Java物件的物件頭中(儲存的指標的指向),synchronized鎖便是通過這種方式獲取鎖的,也是為什麼Java中任意物件可以作為鎖的原因,同時也是notify/notifyAll/wait等方法存在於頂級物件Object中的原因(關於這點稍後還會進行分析),ok~,有了上述知識基礎後,下面我們將進一步分析synchronized在位元組碼層面的具體語義實現。 ## synchronized程式碼塊底層原理 現在我們重新定義一個synchronized修飾的同步程式碼塊, 在程式碼塊中操作共享變數i。 ```java public class SyncCodeBlock { public int i; public void syncTask(){ //同步程式碼庫 synchronized (this){ i++; } } } ``` 編譯上述程式碼,並使用javap反編譯得到位元組碼如下(這裡我們省略一部分沒有必要的資訊): ``` Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class Last modified 2017-6-2; size 426 bytes MD5 checksum c80bc322c87b312de760942820b4fed5 Compiled from "SyncCodeBlock.java" public class com.zejian.concurrencys.SyncCodeBlock minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: //........省略常量池中資料 //建構函式 public com.zejian.concurrencys.SyncCodeBlock(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return LineNumberTable: line 7: 0 //===========主要看看syncTask方法實現================ public void syncTask(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter //注意此處,進入同步方法 4: aload_0 5: dup 6: getfield #2 // Field i:I 9: iconst_1 10: iadd 11: putfield #2 // Field i:I 14: aload_1 15: monitorexit //注意此處,退出同步方法 16: goto 24 19: astore_2 20: aload_1 21: monitorexit //注意此處,退出同步方法 22: aload_2 23: athrow 24: return Exception table: //省略其他位元組碼....... } SourceFile: "SyncCodeBlock.java" ``` 我們主要關注位元組碼中的如下程式碼: ``` 3: monitorenter //進入同步方法 //..........省略其他 15: monitorexit //退出同步方法 16: goto 24 //省略其他....... 21: monitorexit //退出同步方法 ``` 從位元組碼中可知同步語句塊的實現使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步程式碼塊的開始位置,monitorexit指令則指明同步程式碼塊的結束位置,當執行monitorenter指令時,當前執行緒將試圖獲取 objectref(即物件鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor 的進入計數器為 0,那執行緒可以成功取得 monitor,並將計數器值設定為 1,取鎖成功。如果當前執行緒已經擁有 objectref 的 monitor 的持有權,那它可以重入這個 monitor (關於重入性稍後會分析),重入時計數器的值也會加 1。倘若其他執行緒已經擁有 objectref 的 monitor 的所有權,那當前執行緒將被阻塞,直到正在執行執行緒執行完畢,即monitorexit指令被執行,執行執行緒將釋放 monitor(鎖)並設定計數器值為0 ,其他執行緒將有機會持有 monitor 。值得注意的是編譯器將會確保無論方法通過何種方式完成,方法中呼叫過的每條 monitorenter 指令都有執行其對應 monitorexit 指令,而無論這個方法是正常結束還是異常結束。為了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器宣告可處理所有的異常,它的目的就是用來執行 monitorexit 指令。從位元組碼中也可以看出多了一個monitorexit指令,它就是異常結束時被執行的釋放monitor 的指令。 ## synchronized方法底層原理 方法級的同步是隱式,即無需通過位元組碼指令來控制的,它實現在方法呼叫和返回操作之中。JVM可以從方法常量池中的方法表結構(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標誌區分一個方法是否同步方法。當方法呼叫時,呼叫指令將會 檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設定,如果設定了,執行執行緒將先持有monitor(虛擬機器規範中用的是管程一詞), 然後再執行方法,最後再方法完成(無論是正常完成還是非正常完成)時釋放monitor。在方法執行期間,執行執行緒持有了monitor,其他任何執行緒都無法再獲得同一個monitor。如果一個同步方法執行期間拋 出了異常,並且在方法內部無法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法之外時自動釋放。下面我們看看位元組碼層面如何實現: ```java public class SyncMethod { public int i; public synchronized void syncTask(){ i++; } } ``` 使用javap反編譯後的位元組碼如下: ```java Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncMethod.class Last modified 2017-6-2; size 308 bytes MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94 Compiled from "SyncMethod.java" public class com.zejian.concurrencys.SyncMethod minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool; //省略沒必要的位元組碼 //==================syncTask方法====================== public synchronized void syncTask(); descriptor: ()V //方法標識ACC_PUBLIC代表public修飾,ACC_SYNCHRONIZED指明該方法為同步方法 flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=3, locals=1, args_size=1 0: aload_0 1: dup 2: getfield #2 // Field i:I 5: iconst_1 6: iadd 7: putfield #2 // Field i:I 10: return LineNumberTable: line 12: 0 line 13: 10 } SourceFile: "SyncMethod.java" ``` 從位元組碼中可以看出,synchronized修飾的方法並沒有monitorenter指令和monitorexit指令,取得代之的確實是ACC_SYNCHRONIZED標識,該標識指明瞭該方法是一個同步方法,JVM通過該ACC_SYNCHRONIZED訪問標誌來辨別一個方法是否宣告為同步方法,從而執行相應的同步呼叫。這便是synchronized鎖在同步程式碼塊和同步方法上實現的基本原理。同時我們還必須注意到的是在Java早期版本中,synchronized屬於重量級鎖,效率低下,因為監視器鎖(monitor)是依賴於底層的作業系統的Mutex Lock來實現的,而作業系統實現執行緒之間的切換時需要從使用者態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是為什麼早期的synchronized效率低的原因。慶幸的是在Java 6之後Java官方對從JVM層面對synchronized較大優化,所以現在的synchronized鎖效率也優化得很不錯了,Java 6之後,為了減少獲得鎖和釋放鎖所帶來的效能消耗,引入了輕量級鎖和偏向鎖,接下來我們將簡單瞭解一下Java官方在JVM層面對synchronized鎖的優化。 ## Java虛擬機器對synchronized的優化 鎖的狀態總共有四種,無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖。 但鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級。 關於重量級鎖,前面我們已經詳細分析過,下面我們將介紹偏向鎖和輕量級鎖以及JVM的其他優化手段,這裡並不打算深入到每個鎖的實現和轉換過程,更多地是闡述Java虛擬機器提供到每個鎖的核心優化思想。 ### 偏向鎖 偏向鎖是Java 6之後加入的新鎖,它是一種針對加鎖操作的優化手段,經過研究發現,在大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,因此為了減少同一執行緒獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,如果一個執行緒獲得了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變為偏向鎖結構,當這個執行緒再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作,從而也就提供程式的效能。所以,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個執行緒申請相同的鎖。但是對於鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的執行緒都是不相同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗後,並不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。下面我們接著瞭解輕量級鎖。 ### 輕量級鎖 倘若偏向鎖失敗,虛擬機器並不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優化手段(1.6之後加入的),此時Mark Word 的結構也變為輕量級鎖的結構。輕量級鎖能夠提升程式效能的依據是“對絕大部分的鎖,在整個同步週期內都不存在競爭”,注意這是經驗資料。需要了解的是,輕量級鎖所適應的場景是執行緒交替執行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導致輕量級鎖膨脹為重量級鎖。 ### 自旋鎖 輕量級鎖失敗後,虛擬機器為了避免執行緒真實地在作業系統層面掛起,還會進行一項稱為自旋鎖的優化手段。這是基於在大多數情況下,執行緒持有鎖的時間都不會太長,如果直接掛起作業系統層面的執行緒可能會得不償失,畢竟作業系統實現執行緒之間的切換時需要從使用者態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,因此自旋鎖會假設在不久將來,當前的執行緒可以獲得鎖,因此虛擬機器會讓當前想要獲取鎖的執行緒做幾個空迴圈(這也是稱為自旋的原因),一般不會太久,可能是50個迴圈或100迴圈,在經過若干次迴圈後,如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會將執行緒在作業系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是可以提升效率的。最後沒辦法也就只能升級為重量級鎖了。 ### 鎖消除 消除鎖是虛擬機器另外一種鎖的優化,這種優化更徹底,Java虛擬機器在JIT編譯時(可以簡單理解為當某段程式碼即將第一次被執行時進行編譯,又稱即時編譯),通過對執行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間,如下StringBuffer的append是一個同步方法,但是在add方法中的StringBuffer屬於一個區域性變數,並且不會被其他執行緒所使用,因此StringBuffer不可能存在共享資源競爭的情景,JVM會自動將其鎖消除。 ``` /** * Created by zejian on 2017/6/4. * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創] * 消除StringBuffer同步鎖 */ public class StringBufferRemoveSync { public void add(String str1, String str2) { //StringBuffer是執行緒安全,由於sb只會在append方法中使用,不可能被其他執行緒引用 //因此sb屬於不可能共享的資源,JVM會自動消除內部的鎖 StringBuffer sb = new StringBuffer(); sb.append(str1).append(str2); } public static void main(String[] args) { StringBufferRemoveSync rmsync = new StringBufferRemoveSync(); for (int i = 0; i < 10000000; i++) { rmsync.add("abc", "123"); } } } ``` ### 無鎖->偏向鎖 ![](https://mmbiz.qpic.cn/mmbiz_png/SoGf97KLurAicd1Y2Vmx8AMVibe6YvE8giahbLO6jDZkMbiaOpkCU63biaAyZ89MP91WNe4JzDxyI3HhFXm5vUaNY7g/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) 1. 首先A 執行緒訪問同步程式碼塊,使用CAS 操作將 Thread ID 放到 Mark Word 當中; 2. 如果CAS 成功,此時執行緒A 就獲取了鎖 3. 如果執行緒CAS 失敗,證明有別的執行緒持有鎖,例如上圖的執行緒B 來CAS 就失敗的,這個時候啟動偏向鎖撤銷 (revoke bias); 4. 鎖撤銷流程:- 讓 A執行緒在全域性安全點阻塞(類似於GC前執行緒在安全點阻塞) - 遍歷執行緒棧,檢視是否有被鎖物件的鎖記錄( Lock Record),如果有Lock Record,需要修復鎖記錄和Markword,使其變成無鎖狀態。- 恢復A執行緒 - 將是否為偏向鎖狀態置為 0 ,開始進行輕量級加鎖流程 (後面講述) ### 偏向鎖 -> 輕量級鎖 1. 執行緒A在自己的棧楨中建立鎖記錄 LockRecord。 2. 執行緒A 將 Mark Word 拷貝到執行緒棧的 Lock Record中,這個位置叫 displayced hdr,如下圖所示: ![](https://mmbiz.qpic.cn/mmbiz_png/SoGf97KLurAicd1Y2Vmx8AMVibe6YvE8giaLjST0XfiarbR6vlTRgVfrR9z91nLlhh0bGHekbQ3Libl6RrWcHml7rdA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) 3. 將鎖記錄中的Owner指標指向加鎖的物件(存放物件地址)。 4. 將鎖物件的物件頭的MarkWord替換為指向鎖記錄的指標。這二步如下圖所示: ![](https://mmbiz.qpic.cn/mmbiz_png/SoGf97KLurAicd1Y2Vmx8AMVibe6YvE8giazHicpdHpADZMibb8N7nJexCxpMXL5076bTc0tibuC6xaJRdMVf9RSIA8Q/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) 這時鎖標誌位變成 00 ,表示輕量級鎖 ### 輕量級鎖 -> 重量級鎖 當鎖升級為輕量級鎖之後,如果依然有新執行緒過來競爭鎖,首先新執行緒會自旋嘗試獲取鎖,嘗試到一定次數(預設10次)依然沒有拿到,鎖就會升級成重量級鎖. ![](https://mmbiz.qpic.cn/mmbiz_png/SoGf97KLurAicd1Y2Vmx8AMVibe6YvE8gia5ExIwuCLlcVOLOcM1IFZYvWoJMhQUias2KUg4etbTJMdvicc8HXEcTpQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) 1. 將 MonitorObject 中的 _owner設定成 A執行緒; 2. 將 mark word 設定為 Monitor 物件地址,鎖標誌位改為10 3. 將B執行緒阻塞放到 ContentionList 佇列; JVM 每次從Waiting Queue 的尾部取出一個執行緒放到OnDeck作為候選者,但是如果併發比較高,Waiting Queue會被大量執行緒執行CAS操作,為了降低對尾部元素的競爭,將Waiting Queue 拆分成ContentionList 和 EntryList 二個佇列, JVM將一部分執行緒移到EntryList 作為準備進OnDeck的預備執行緒。另外說明幾點: 所有請求鎖的執行緒首先被放在ContentionList這個競爭佇列中; Contention List 中那些有資格成為候選資源的執行緒被移動到 Entry List 中; 任意時刻,最多隻有一個執行緒正在競爭鎖資源,該執行緒被成為 OnDeck; 當前已經獲取到所資源的執行緒被稱為 Owner; 處於 ContentionList、EntryList、WaitSet 中的執行緒都處於阻塞狀態,該阻塞是由作業系統來完成的(Linux 核心下采用 `pthread_mutex_lock` 核心函式實現的); 作為Owner 的A 執行緒執行過程中,可能呼叫wait 釋放鎖,這個時候A執行緒進入 Wait Set , 等待被喚醒。 這是 synchronized 在 JDK 6之前的實現原理。 ## 關於synchronized 可能需要了解的關鍵點 ### synchronized的可重入性 從互斥鎖的設計上來說,當一個執行緒試圖操作一個由其他執行緒持有的物件鎖的臨界資源時,將會處於阻塞狀態,但當一個執行緒再次請求自己持有物件鎖的臨界資源時,這種情況屬於重入鎖,請求將會成功,在java中synchronized是基於原子性的內部鎖機制,是可重入的,因此在一個執行緒呼叫synchronized方法的同時在其方法體內部呼叫該物件另一個synchronized方法,也就是說一個執行緒得到一個物件鎖後再次請求該物件鎖,是允許的,這就是synchronized的可重入性。如下: ```java public class AccountingSync implements Runnable{ static AccountingSync instance=new AccountingSync(); static int i=0; static int j=0; @Override public void run() { for(int j=0;j<1000000;j++){ //this,當前例項物件鎖 synchronized(this){ i++; increase();//synchronized的可重入性 } } } public synchronized void increase(){ j++; } public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(instance); Thread t2=new Thread(instance); t1.start();t2.start(); t1.join();t2.join(); System.out.println(i); } } ``` 正如程式碼所演示的,在獲取當前例項物件鎖後進入synchronized程式碼塊執行同步程式碼,並在程式碼塊中呼叫了當前例項物件的另外一個synchronized方法,再次請求當前例項鎖時,將被允許,進而執行方法體程式碼,這就是重入鎖最直接的體現,需要特別注意另外一種情況,當子類繼承父類時,子類也是可以通過可重入鎖呼叫父類的同步方法。注意由於synchronized是基於monitor實現的,因此每次重入,monitor中的計數器仍會加1。 ## 執行緒中斷與synchronized ### 執行緒中斷 正如中斷二字所表達的意義,線上程執行(run方法)中間打斷它,在Java中,提供了以下3個有關執行緒中斷的方法 ```java //中斷執行緒(例項方法) public void Thread.interrupt(); //判斷執行緒是否被中斷(例項方法) public boolean Thread.isInterrupted(); //判斷是否被中斷並清除當前中斷狀態(靜態方法) public static boolean Thread.interrupted(); ``` 當一個執行緒處於被阻塞狀態或者試圖執行一個阻塞操作時,使用Thread.interrupt()方式中斷該執行緒,注意此時將會丟擲一個InterruptedException的異常,同時中斷狀態將會被複位(由中斷狀態改為非中斷狀態),如下程式碼將演示該過程: ```java public class InterruputSleepThread3 { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread() { @Override public void run() { //while在try中,通過異常中斷就可以退出run迴圈 try { while (true) { //當前執行緒處於阻塞狀態,異常必須捕捉處理,無法往外丟擲 TimeUnit.SECONDS.sleep(2); } } catch (InterruptedException e) { System.out.println("Interruted When Sleep"); boolean interrupt = this.isInterrupted(); //中斷狀態被複位 System.out.println("interrupt:"+interrupt); } } }; t1.start(); TimeUnit.SECONDS.sleep(2); //中斷處於阻塞狀態的執行緒 t1.interrupt(); /** * 輸出結果: Interruted When Sleep interrupt:false */ } } ``` 如上述程式碼所示,我們建立一個執行緒,並在執行緒中呼叫了sleep方法從而使用執行緒進入阻塞狀態,啟動執行緒後,呼叫執行緒例項物件的interrupt方法中斷阻塞異常,並丟擲InterruptedException異常,此時中斷狀態也將被複位。這裡有些人可能會詫異,為什麼不用Thread.sleep(2000);而是用TimeUnit.SECONDS.sleep(2);其實原因很簡單,前者使用時並沒有明確的單位說明,而後者非常明確表達秒的單位,事實上後者的內部實現最終還是呼叫了Thread.sleep(2000);,但為了編寫的程式碼語義更清晰,建議使用TimeUnit.SECONDS.sleep(2);的方式,注意TimeUnit是個列舉型別。ok~,除了阻塞中斷的情景,我們還可能會遇到處於執行期且非阻塞的狀態的執行緒,這種情況下,直接呼叫Thread.interrupt()中斷執行緒是不會得到任響應的,如下程式碼,將無法中斷非阻塞狀態下的執行緒: ## 等待喚醒機制與synchronized 所謂等待喚醒機制本篇主要指的是notify/notifyAll和wait方法,在使用這3個方法時,必須處於synchronized程式碼塊或者synchronized方法中,否則就會丟擲IllegalMonitorStateException異常,這是因為呼叫這幾個方法前必須拿到當前物件的監視器monitor物件,也就是說notify/notifyAll和wait方法依賴於monitor物件,在前面的分析中,我們知道monitor 存在於物件頭的Mark Word 中(儲存monitor引用指標),而synchronized關鍵字可以獲取 monitor ,這也就是為什麼notify/notifyAll和wait方法必須在synchronized程式碼塊或者synchronized方法呼叫的原因。 ```java synchronized (obj) { obj.wait(); obj.notify(); obj.notifyAll(); } ``` 需要特別理解的一點是,與sleep方法不同的是wait方法呼叫完成後,執行緒將被暫停,但wait方法將會釋放當前持有的監視器鎖(monitor),直到有執行緒呼叫notify/notifyAll方法後方能繼續執行,而sleep方法只讓執行緒休眠並不釋放鎖。同時notify/notifyAll方法呼叫後,並不會馬上釋放監視器鎖,而是在相應的synchronized(){}/synchronized方法執行結束後才自動釋放鎖。 ## 來源 - https://blog.csdn.net/javazejian/article/details/72828483 - https://mp.weixin.qq.com/s/ts2Pjz3VpWm50k