synchronized 作用
synchronized 關鍵字是 Java 并發編程中線程同步的常用手段之一。
1.1 作用:
- 確保線程互斥的訪問同步代,鎖自動釋放,多個線程操作同個代碼塊或函數必須排隊獲得鎖,
- 保證共享變量的修改能夠及時可見,獲得鎖的線程操作完畢后會將所數據刷新到共享內存區;
- 不解決重排序,但保證有序性。
1.2 用法:
- 修飾實例方法synchronized 關鍵詞作用在方法的前面,用來鎖定方法,其實默認鎖定的是 this 對象。
public class Thread1 implements Runnable{ //共享資源(臨界資源) static int i=0; //如果沒有synchronized關鍵字,輸出小于20000 public synchronized void increase(){ i++; } public void run() { for(int j=0;j<10000;j++){ increase(); } } public static void main(String[] args) throws InterruptedException { Thread1 t=new Thread1(); Thread t1=new Thread(t); Thread t2=new Thread(t); t1.start(); t2.start(); t1.join();//主線程等待t1執行完畢 t2.join();//主線程等待t2執行完畢 System.out.println(i); }}

- 修飾靜態方法synchronized 還是修飾在方法上,不過修飾的是靜態方法,等價于鎖定的是 Class 對象。
public class Thread1 { //共享資源(臨界資源) static int i = 0; //如果沒有synchronized關鍵字,輸出小于20000 public static synchronized void increase() { i++; } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Runnable() { public void run() { for (int j = 0; j < 10000; j++) { increase(); } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 10000; j++) { increase(); } } }); t1.start(); t2.start(); t1.join();//主線程等待t1執行完畢 t2.join();//主線程等待t2執行完畢 System.out.println(i); }}

- 修飾代碼塊用法是在函數體內部對于要修改的參數區間用 synchronized 來修飾,相比與鎖定函數這個范圍更小,可以指定鎖定什么對象。
public class Thread1 implements Runnable { //共享資源(臨界資源) static int i = 0; @Override public void run() { for (int j = 0; j < 10000; j++) { //獲得了String的類鎖 synchronized (String.class) { i++; } } } public static void main(String[] args) throws InterruptedException { Thread1 t = new Thread1(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); }}

總結:
- synchronized 修飾的實例方法,多線程并發訪問時,只能有一個線程進入,獲得對象內置鎖,其他線程阻塞等待,但在此期間線程仍然可以訪問其他方法。
管程
管程(英語:Monitors,也稱為監視器) 在操作系統中是很重要的概念,管程其實指的是管理共享變量以及管理共享變量的操作過程。有點扮演中介的意思,管程管理一堆對象,多個線程同一時候只能有一個線程來訪問這些東西。
管程可以看做一個軟件模塊,它是將共享的變量和對于這些共享變量的操作封裝起來,形成一個具有一定接口的功能模塊,進程可以調用管程來實現進程級別的并發控制。
進程只能互斥地使用管程,即當一個進程使用管程時,另一個進程必須等待。當一個進程使用完管程后,它必須釋放管程并喚醒等待管程的某一個進程。
管程解決互斥問題相對簡單,需要把共享變量以及共享變量的操作都封裝在一個類中。

當線程 A 和線程 B 需要獲取共享變量 count 時,就需要調用 get 和 set 方法,而 get 和 set 方法則保證互斥性,保證每次只能有一個線程訪問。
生活中舉例管程,比如鏈家店長分配給每個中介管理一部分二手房源,多個客戶通過中介進行房屋買賣。
- 中介就是管程。
- 多個二手房源被一個中介管理中,就是一個管程管理著多個系統資源。
- 多個客戶就相當于多個線程。
Synchronzied 的底層原理
對象頭解析
我們知道在 Java 的 JVM 內存區域中一個對象在堆區創建,創建后的對象由對象頭、實例變量、填充數據三部分組成。這三部分功能如下:
- 填充數據:由于虛擬機要求對象起始地址必須是 8 字節的整數倍。填充數據不是必須存在的,僅僅是為了字節對齊。

Klass Point (類型指針):是對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
Mark Word (標記字段):這一部分用于儲存對象自身的運行時的數據,如哈希碼、GC 分代年齡、鎖狀態標志、鎖指針等。這部分數據在 32 bit 和 64 bit 的虛擬機中大小分別為 32 bit 和 64 bit,考慮到虛擬機的空間效率,Mark Word 被設計成一個非固定的數據結構,以便在極小的空間中存儲盡量多的信息,它會根據對象的狀態復用自己的存儲空間(跟 ConcurrentHashMap 里的標志位類似),詳細情況如下圖:

synchronized 不論是修飾方法還是代碼塊,都是通過持有修飾對象的鎖來實現同步,synchronized 鎖對象是存在對象頭 Mark Word。
其中,輕量級鎖和偏向鎖是 Java6 對 synchronized 鎖進行優化后新增加的。這里我們主要分析一下重量級鎖,也就是通常說 synchronized 的對象鎖。所標識位為 10,其中指針指向的是 monitor 對象(也稱為管程或監視器鎖)的起始地址。每個對象都存在著一個 monitor 與之關聯。

反匯編查看
分析對象的 monitor 前我們先通過反匯編看下同步方法跟同步方法塊在匯編語言級別是什么樣的指令。
public class SynchronizedTest { public synchronized void doSth(){ System.out.println("Hello World"); } public void doSth1(){ synchronized (SynchronizedTest.class){ System.out.println("Hello World"); } }}
javac SynchronizedTest .java 然后 javap -c SynchronizedTest 反編譯后看匯編指令如下:
public synchronized void doSth(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 這是重點 方法鎖 Code: stack=2, locals=1, args_size=1 0: getstatic #2 3: ldc #3 5: invokevirtual #4 8: return public void doSth1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: ldc #5 2: dup 3: astore_1 4: monitorenter // 進入同步方法 5: getstatic #2 8: ldc #3 10: invokevirtual #4 13: aload_1 14: monitorexit //正常時 退出同步方法 15: goto 23 18: astore_2 19: aload_1 20: monitorexit // 異常時 退出同步方法 21: aload_2 22: athrow 23: return
我們可以看到 Java 編譯器為我們生成的字節碼。在對于 doSth 和 doSth1 的處理上稍有不同。也就是說,JVM 對于同步方法和同步代碼塊的處理方式不同。對于同步方法,JVM 采用ACC_SYNCHRONIZED 標記符來實現同步。對于同步代碼塊。JVM 采用 monitorenter、monitorexit 兩個指令來實現同步。
ACC_SYNCHRONIZED
方法級的同步是隱式的。
同步方法的常量池中會有一個 ACC_SYNCHRONIZED 標志。當某個線程要訪問某個方法的時候,會檢查是否有 ACC_SYNCHRONIZED,如果有設置,則需要先獲得監視器鎖,然后開始執行方法,方法執行之后再釋放監視器鎖。
這時如果其他線程來請求執行方法,會因為無法獲得監視器鎖而被阻斷住。值得注意的是,如果在方法執行過程中發生了異常,并且方法內部并沒有處理該異常,那么在異常被拋到方法外面之前監視器鎖會被自動釋放。
monitorenter 跟 monitorexit
可以把執行 monitorenter 指令理解為加鎖,執行 monitorexit 理解為釋放鎖。
每個對象維護著一個記錄著被鎖次數的計數器。未被鎖定的對象的該計數器為 0,當一個線程獲得鎖(執行 monitorenter )后,該計數器自增變為 1 ,當同一個線程再次獲得該對象的鎖的時候,計數器再次自增。當同一個線程釋放鎖(執行 monitorexit 指令)的時候,計數器再自檢。
當計數器為 0 的時候。鎖將被釋放,其他線程便可以獲得鎖。
結論:
同步方法和同步代碼塊底層都是通過 monitor 來實現同步的。兩者區別:同步方式是通過方法中的 access_flags 中設置 ACC_SYNCHRONIZED 標志來實現,同步代碼塊是通過 monitorenter 和 monitorexit 來實現。
monitor 解析
每個對象都與一個 monitor 相關聯,而 monitor 可以被線程擁有或釋放,在Java 虛擬機( HotSpot )中,monitor 是由 ObjectMonitor 實現的,其主要數據結構如下(位于 HotSpot 虛擬機源碼 ObjectMonitor.hpp 文件,C++實現的)。
ObjectMonitor() { _count = 0; //記錄數 _recursions = 0; //鎖的重入次數 _owner = NULL; //指向持有ObjectMonitor對象的線程 _WaitSet = NULL; //調用wait后,線程會被加入到_WaitSet _EntryList = NULL ; //等待獲取鎖的線程,會被加入到該列表}
monitor 運行圖如下:

對于一個 synchronized 修飾的方法(代碼塊)來說:
- 當多個線程同時訪問該方法,那么這些線程會先被放進_EntryList 隊列,此時線程處于 blocked 狀態;
- 當一個線程獲取到了對象的 monitor 后,那么就可以進入 running 狀態,執行方法塊,此時,ObjectMonitor 對象的_owner 指向當前線程,_count 加 1 表示當前對象鎖被一個線程獲取;
- 當 running 狀態的線程調用 wait() 方法,那么當前線程釋放 monitor 對象,進入 waiting 狀態,ObjectMonitor 對象的_owner 變為 null,_count 見 1,同時線程進入_WaitSet 隊列,直到有線程調用 notify() 方法喚醒該線程,則該線程進入_EntryList 隊列,競爭到鎖再進入_owner區;
- 如果當前線程執行完畢,那么也釋放 monitor 對象,ObjectMonitor 對象的_owner 變為 null,_count 見 1。
因為監視器鎖(monitor)是依賴于底層的操作系統的 Mutex Lock 來實現的,而操作系統實現線程之間的切換時需要從用戶態轉換到核心態(具體可看CXUAN 寫的 OS 哦),這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是早期的 synchronized 效率低的原因。慶幸在 Java 6 之后Java 官方對從 JVM 層面對 synchronized 較大優化最終提升顯著,Java 6 之后,為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了鎖升級的概念。
鎖升級
synchronized 鎖有四種狀態:無鎖、偏向鎖、輕量級鎖、重量級鎖。
這幾個狀態會隨著競爭狀態逐漸升級,鎖可以升級但不能降級,但是偏向鎖狀態可以被重置為無鎖狀態。科學性的說這些鎖之前我們先看個簡單通俗的例子來加深印象。
通俗說法理解各種鎖
偏向鎖、輕量級鎖和重量級鎖之間的關系,首先打個比方:假設現在廁所只有一個位置,每個使用者都有打開門鎖的鑰匙。必須打開門鎖才能使用廁所。其中小明、小紅理解為兩個線程,上廁所理解為執行同步代碼,門鎖理解為同步代碼的鎖
- 小明今天吃壞了東西需要反復去廁所,如果小明每次都要開鎖就很耽誤時間,于是門鎖將小明的臉記錄下來(假設那個鎖是智能鎖),下次小明再來的時候門鎖會自動識別出是小明來了,然后自動開鎖,這樣就省去了小明拿鑰匙開門的過程,此時門鎖就是偏向鎖,也可以理解為偏向小明的鎖。
結論:
偏向鎖在只有一個人上廁所時非常高效,省去了開門的過程。
輕量級鎖在有多人上廁所但是每個人使用的特別快的時候,比較高效,因為會出現這種現象,小紅敲門的時候正好趕上小明出來,這樣就省得小明出來告訴小紅以后小紅才能進去,但是這樣可能會出現小紅敲門失敗的情況(就是敲門時小明還沒用完)。
重量級鎖相比與輕量級鎖的多了一步小明呼喚小紅的步驟,但是卻省掉了小紅反復去敲門的過程,但是能保證小紅去廁所時廁所一定是沒人的。
偏向鎖
經過 HotSpot 作者大量的研究發現:大多數時候是不存在鎖競爭的,經常是一個線程多次獲得同一個鎖,因此如果每次都要競爭鎖會增大很多沒有必要付出的代價,為了降低獲取鎖的代價,才引入的偏向鎖。核心思想:
如果一個線程獲得了鎖,那么鎖就進入偏向模式,此時 Mark Word 的結構也變為偏向鎖結構,當這個線程再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程。
這樣就省去了大量有關鎖申請的操作,從而也就提供程序的性能。所以,對于沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個線程申請相同的鎖。但是對于鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的線程都是不相同的。
因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗后,并不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。
具體流程:當線程 1 訪問代碼塊并獲取鎖對象時,會在 java 對象頭和棧幀中記錄偏向的鎖的 threadID,因為偏向鎖不會主動釋放鎖。因此以后線程 1 再次獲取鎖的時候,需要比較當前線程的 threadID 和 Java 對象頭中的threadID 是否一致,如果一致(還是線程 1 獲取鎖對象),則無需使用 CAS 來加鎖、解鎖;如果不一致(其他線程,如線程 2 要競爭鎖對象,而偏向鎖不會主動釋放因此還是存儲的線程 1 的 threadID),那么需要查看Java 對象頭中記錄的線程 1 是否存活,如果沒有存活,那么鎖對象被重置為無鎖狀態,其它線程(線程 2)可以競爭將其設置為偏向鎖;如果存活,那么立刻查找該線程(線程 1)的棧幀信息,如果還是需要繼續持有這個鎖對象,那么暫停當前線程 1,撤銷偏向鎖,升級為 輕量級鎖,如果線程 1 不再使用該鎖對象,那么將鎖對象狀態設為無鎖狀態,重新偏向新的線程。
輕量級鎖
輕量級鎖考慮的是競爭鎖對象的線程不多,而且線程持有鎖的時間也不長的情景。因為阻塞線程需要高昂的耗時實現 CPU 從用戶態轉到內核態的切換,如果剛剛阻塞不久這個鎖就被釋放了,那這個代價就有點得不償失了,因此這個時候就干脆不阻塞這個線程,讓它自旋這等待鎖釋放。
原理跟升級:線程 1 獲取輕量級鎖時會先把鎖對象的對象頭 MarkWord 復制一份到線程 1 的棧幀中創建的用于存儲鎖記錄的空間(稱為DisplacedMarkWord ),然后使用 CAS 把對象頭中的內容替換為線程 1 存儲的鎖記錄(DisplacedMarkWord)的地址;
如果在線程 1 復制對象頭的同時(在線程 1 CAS 之前),線程 2 也準備獲取鎖,復制了對象頭到線程 2 的鎖記錄空間中,但是在線程 2 CAS 的時候,發現線程 1 已經把對象頭換了,「線程 2 的 CAS 失敗,那么線程 2 就嘗試使用自旋鎖來等待線程 1 釋放鎖」。自旋鎖簡單來說就是讓線程 2 在循環中不斷 CAS 嘗試獲得鎖對象。
但是如果自旋的時間太長也不行,因為自旋是要消耗 CPU 的,因此自旋的次數是有限制的。比如 10 次或者 100 次,如果自旋次數到了線程 1 還沒有釋放鎖,或者線程 1 還在執行,線程 2 還在自旋等待,那么這個時候輕量級鎖就會膨脹為重量級鎖。重量級鎖把除了擁有鎖的線程都阻塞,防止 CPU 空轉。

鎖消除
消除鎖是虛擬機另外一種鎖的優化,這種優化更徹底,Java 虛擬機在 JIT 編譯時通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消,除沒有必要的鎖,可以節省毫無意義的請求鎖時間,我們知道StringBuffer 是線程安全的,里面包含鎖的存在,但是如果我們在函數內部使用 StringBuffer 那么代碼會在 JIT 后會自動將鎖釋放掉哦。
對比如下:
鎖狀態優點缺點適用場景偏向鎖加鎖解鎖無需額外消耗,跟非同步方法時間相差納秒級別如果競爭線程多,會帶來額外的鎖撤銷的消耗基本沒有其他線程競爭的同步場景輕量級鎖競爭的線程不會阻塞而是在自旋,可提高程序響應速度如果一直無法獲得會自旋消耗CPU少量線程競爭,持有鎖時間不長,追求響應速度重量級鎖線程競爭不會導致 CPU 自旋跟消耗 CPU 資源線程阻塞,響應時間長很多線程競爭鎖,切鎖持有時間長,追求吞吐量時候
PS:ReentrantLock 底層實現依賴于特殊的 CPU 指令,比如發送 lock 指令和 unlock 指令,不需要用戶態和內核態的切換,所以效率高。而synchronized 底層由監視器鎖(monitor)是依賴于底層的操作系統的Mutex Lock 需要用戶態和內核態的切換,所以效率會低一些。
鎖升級流程圖
最后奉上 unbelievableme 繪制的鎖升級大圖。
