Synchronized底層原理
sync鎖的是什麼?它是怎麼就把對象給鎖上了?
是不是也應該有個變量來控制呢?
上鎖就是改變對象的對象頭
java對象頭是實作synchronized的鎖對象的基礎,synchronized使用的鎖對象是存儲在Java對象頭裡的。
那麼問題來了。對象頭又是什麼鬼?
什麼是對象頭?
這就要知道java的對象布局,換句話說就是java對象由什麼組成。
在JVM中,對象在記憶體中的布局分為三塊區域:對象頭、執行個體資料和對齊填充。
1、對象頭 ------(大小固定)
2、執行個體資料 ------- (大小不固定)
3、對齊填充

- 執行個體變量:存放類的屬性資料資訊,包括父類的屬性資訊,如果是數組的執行個體部分還包括數組的長度,這部分記憶體按4位元組對齊。
- 填充資料:對齊填充并不是必然存在的,也沒有特别的含義,它僅僅起着占位符的作用。由于HotSpot VM的自動記憶體管理系統要求對象起始位址必須是8位元組的整數倍,換句話說就是對象的大小必須是8位元組的整數倍。對象頭正好是8位元組的倍數(1倍或者2倍),是以當對象執行個體資料部分沒有對齊的話,就需要通過對齊填充來補全。
- 對象頭:包括運作時中繼資料(Mark Word)和 類型指針(Class Metadata Address)
鎖狀态 25bit 4bit 1bit是否是偏向鎖 2bit 鎖标志位 無鎖狀态 對象HashCode GC分代年齡 01 - 運作時中繼資料
- 哈希值( HashCode )
- GC分代年齡
- 鎖狀态标志
- 線程持有的鎖
- 偏向線程ID
- 偏向時間戳
-
類型指針:指向類中繼資料的InstanceClass,确定該對象所屬的類型
說明:如果是數組,還需記錄數組的長度
- 運作時中繼資料
其中輕量級鎖和偏向鎖是Java 6 對 synchronized 鎖進行優化後新增加的,稍後我們會簡要分析。這裡我們主要分析一下重量級鎖也就是通常說synchronized的對象鎖,鎖辨別位為10,其中指針指向的是
monitor對象
(也稱為管程或螢幕鎖)的起始位址。每個對象都存在着一個 monitor 與之關聯,對象與其 monitor 之間的關系有存在多種實作方式,如monitor可以與對象一起建立銷毀或當線程試圖擷取對象鎖時自動生成,但當一個 monitor 被某個線程持有後,它便處于鎖定狀态。在Java虛拟機(HotSpot)中,monitor是由ObjectMonitor實作的,其主要資料結構如下(位于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中的owner變量設定為目前線程,同時monitor中的計數器count加1;
若線程調用 wait() 方法,将釋放目前持有的monitor,owner變量恢複為null,count自減1,同時該線程進入 WaitSet集合中等待被喚醒。
若目前線程執行完畢,也将釋放monitor(鎖)并複位變量的值,以便其他線程進入擷取monitor(鎖)。
如下圖所示:
由此看來,monitor對象存在于每個Java對象的對象頭中(存儲的指針的指向),synchronized鎖便是通過這種方式擷取鎖的,也是為什麼Java中任意對象可以作為鎖的原因,同時也是notify/notifyAll/wait等方法存在于頂級對象Object中的原因(關于這點稍後還會進行分析),ok~,有了上述知識基礎後,下面我們将進一步分析synchronized在位元組碼層面的具體語義實作。
Synchronized标記代碼塊底層原理
public class SyncCodeBlock {
public int i;
public void syncTask(){
//同步代碼庫
synchronized (this){
i++;
}
}
}
編譯後的位元組碼:
3: monitorenter //進入同步方法
//..........省略其他
15: monitorexit //退出同步方法
16: goto 24
//省略其他.......
21: monitorexit //退出同步方法
從位元組碼中可知同步語句塊的實作使用的是
monitorenter
和
monitorexit
指令
其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結束位置.
擷取鎖的過程:
當執行monitorenter指令時,目前線程将試圖擷取 objectref(即對象鎖) 所對應的 monitor 的持有權。
當 objectref 的 monitor 的計數器為 0,那線程可以成功取得 monitor,将monitor對象的owner變量設定為目前線程,并将計數器值設定為 1,取鎖成功。
如果目前線程已經擁有 objectref 的 monitor 的持有權,那它可以重入這個 monitor (關于重入性稍後會分析),重入時計數器的值也會加 1。倘若其他線程已經擁有 objectref 的 monitor 的所有權,那目前線程将被阻塞,直到正在執行線程執行完畢,即monitorexit指令被執行,執行線程将釋放 monitor(鎖)并設定計數器值為0 ,owner設定為null,其他線程将有機會持有 monitor 。
值得注意的是編譯器将會確定無論方法通過何種方式完成,方法中調用過的每條 monitorenter 指令都有執行其對應 monitorexit 指令,而無論這個方法是正常結束還是異常結束。為了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正确配對執行,編譯器會自動産生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執行 monitorexit 指令。從位元組碼中也可以看出多了一個monitorexit指令,它就是異常結束時被執行的釋放monitor 的指令。
synchronized方法底層原理
方法級的同步是隐式,即無需通過位元組碼指令來控制的,它實作在方法調用和傳回操作之中。JVM可以從方法常量池中的方法表結構(method_info Structure) 中的**
ACC_SYNCHRONIZED
通路标志區分一個方法是否同步方法**。當方法調用時,調用指令将會 檢查方法的
ACC_SYNCHRONIZED
通路标志是否被設定,如果設定了,執行線程将先持有monitor對象, 然後再執行方法,最後再方法完成(無論是正常完成還是非正常完成)時釋放monitor。在方法執行期間,執行線程持有了monitor,其他任何線程都無法再獲得同一個monitor。如果一個同步方法執行期間抛 出了異常,并且在方法内部無法處理此異常,那這個同步方法所持有的monitor将在異常抛到同步方法之外時自動釋放。下面我們看看位元組碼層面如何實作:
public class SyncMethod {
public int i;
public synchronized void syncTask(){
i++;
}
}
編譯後的位元組碼:
//省略沒必要的位元組碼
//==================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虛拟機原理》。
更新過程: 偏向鎖 → 輕量級鎖 →(自旋鎖 )→ 重量級鎖
偏向鎖
偏向鎖是Java 6之後加入的新鎖,它是一種針對加鎖操作的優化手段,經過研究發現,在大多數情況下,鎖不僅不存在多線程競争,而且總是由同一線程多次獲得,是以為了減少同一線程擷取鎖(會涉及到一些CAS操作)耗時的代價,而引入偏向鎖。
偏向鎖的核心思想是,如果一個線程獲得了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變為偏向鎖結構,當這個線程再次請求鎖時,無需再做任何同步操作,即擷取鎖的過程,這樣就省去了大量有關鎖申請的操作,進而也就提供程式的性能。
是以,對于沒有鎖競争的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個線程申請相同的鎖。但是對于鎖競争比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的線程都是不相同的,是以這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗後,并不會立即膨脹為重量級鎖,而是先更新為輕量級鎖。下面我們接着了解輕量級鎖。
輕量級鎖
當這樣的場合每次申請鎖的線程都是不相同的,偏向鎖就不适用了,就會更新為輕量級鎖。
為啥叫輕量級鎖,因為這是相比于傳統的重量級鎖而言,原來傳統的重量級鎖,使用的是系統互斥量實作的。
他的出現并不是代替重量級鎖,而是在沒有多線程競争的前提下,減少系統互斥量操作産生的性能消耗。
倘若偏向鎖失敗,虛拟機并不會立即更新為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優化手段(1.6之後加入的),此時Mark Word 的結構也變為輕量級鎖的結構。輕量級鎖能夠提升程式性能的依據是“對絕大部分的鎖,在整個同步周期内都不存在競争”,注意這是經驗資料。如果沒有競争,輕量級鎖使用CAS操作避免了使用互斥量的開銷,但如果存在鎖競争,除了互斥量的開銷外,還額外發生了CAS操作,是以在有競争的情況下,輕量級鎖比傳統的重量級鎖更慢。
需要了解的是,輕量級鎖所适應的場景是線程交替執行同步塊的場合,如果存在同一時間通路同一鎖的場合,就會導緻輕量級鎖膨脹為重量級鎖。
輕量級鎖的加鎖過程:
- 在代碼進入同步塊的時候,如果同步對象鎖狀态為無鎖狀态(鎖标志位為“01”狀态,是否為偏向鎖為“0”),虛拟機首先将在目前線程的棧幀中建立一個名為**鎖記錄(Lock Record)**的空間,用于存儲鎖對象目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word。這時候線程堆棧與對象頭的狀态如圖:
- 拷貝對象頭中的Mark Word複制到鎖記錄(Lock Record)中;
- 拷貝成功後,虛拟機将使用CAS操作嘗試将鎖對象的Mark Word更新為指向Lock Record的指針,并将線程棧幀中的Lock Record裡的owner指針指向Object的 Mark Word。
- 如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖标志位設定為“00”,即表示此對象處于輕量級鎖定狀态。這時候線程堆棧與對象頭的狀态如圖所示。
談一談Synchronized底層原理Synchronized底層原理 - 如果這個更新操作失敗了,虛拟機首先會檢查對象的Mark Word是否指向目前線程的棧幀,如果是就說明目前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行。否則說明多個線程競争鎖,輕量級鎖就要膨脹為重量級鎖,鎖标志的狀态值變為“10”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀态。
自旋鎖
輕量級鎖失敗後,虛拟機為了避免線程真實地在作業系統層面挂起,還會進行一項稱為自旋鎖的優化手段。這是基于在大多數情況下,線程持有鎖的時間都不會太長,如果直接挂起作業系統層面的線程可能會得不償失,畢竟作業系統實作線程之間的切換時需要從使用者态轉換到核心态,這個狀态之間的轉換需要相對比較長的時間,時間成本相對較高,是以自旋鎖會假設在不久将來,目前的線程可以獲得鎖,是以虛拟機會讓目前想要擷取鎖的線程做幾個空循環(這也是稱為自旋的原因),一般不會太久,可能是50個循環或100循環,在經過若幹次循環後,如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會将線程在作業系統層面挂起,這就是自旋鎖的優化方式,這種方式确實也是可以提升效率的。最後沒辦法也就隻能更新為重量級鎖了。
重量級鎖
重量級鎖也就是通常說synchronized的對象鎖,鎖辨別位為10,其中指針指向的是monitor對象(也稱為管程或螢幕鎖)的起始位址。每個對象都存在着一個 monitor 與之關聯,對象與其 monitor 之間的關系有存在多種實作方式,如monitor可以與對象一起建立銷毀或當線程試圖擷取對象鎖時自動生成,但當一個 monitor 被某個線程持有後,它便處于鎖定狀态。
鎖消除
消除鎖是虛拟機另外一種鎖的優化,這種優化更徹底,Java虛拟機在JIT編譯時(可以簡單了解為當某段代碼即将第一次被執行時進行編譯,又稱即時編譯),通過對運作上下文的掃描,去除不可能存在共享資源競争的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間,如下StringBuffer的append是一個同步方法,但是在add方法中的StringBuffer屬于一個局部變量,并且不會被其他線程所使用,是以StringBuffer不可能存在共享資源競争的情景,JVM會自動将其鎖消除。
synchronized的可重入性
從互斥鎖的設計上來說,當一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時,将會處于阻塞狀态,但當一個線程再次請求自己持有對象鎖的臨界資源時,這種情況屬于重入鎖,請求将會成功,在java中synchronized是基于原子性的内部鎖機制,是可重入的,是以在一個線程調用synchronized方法的同時在其方法體内部調用該對象另一個synchronized方法,也就是說一個線程得到一個對象鎖後再次請求該對象鎖,是允許的,這就是synchronized的可重入性。如下:
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
所謂等待喚醒機制本篇主要指的是notify/notifyAll和wait方法,在使用這3個方法時,必須處于synchronized代碼塊或者synchronized方法中,否則就會抛出IllegalMonitorStateException異常,這是因為調用這幾個方法前必須拿到目前對象的螢幕monitor對象,也就是說notify/notifyAll和wait方法依賴于monitor對象,在前面的分析中,我們知道monitor 存在于對象頭的Mark Word 中(存儲monitor引用指針),而synchronized關鍵字可以擷取 monitor ,這也就是為什麼notify/notifyAll和wait方法必須在synchronized代碼塊或者synchronized方法調用的原因。