[Java多執行緒 -2]:由淺入深看synchronized的底層實現原理
前倆篇文章,我們聊了聊執行緒/程序的概念,接著簡單串了一下同步的方式方法。今天我們就單拎出來synchronized,好好捋一捋它的前世今生。
正文
小A:咱們前幾天鋪墊了這麼多內容,今天是不是要好好的深挖一下原理的內容了?
MDove:沒錯,接下來。我會從常見的synchronized加鎖方式入手;引出Java物件在記憶體的佈局,以及鎖的存放位置;然後看一看鎖在C++中的簡單實現思路;最後咱們從位元組碼中,看一下JVM如果識別synchronized。內容不是很難,不會涉及到特別多深奧的內容,大部分是平鋪直敘的介紹,很適合閱讀呦~
小A:快點開始吧,我等不及啦。
淺聊synchronized的使用
MDove:說起synchronized的底層實現原來,咱們先看看synchronized的倆種加鎖方式:
1、某個物件例項內
此作用域內的synchronized鎖 ,可以防止多個執行緒同時訪問這個物件的synchronized方法
並且一個物件有多個synchronized方法,只要一個執行緒訪問了其中的一個synchronized方法,其它執行緒不能同時訪問這個物件中任何一個synchronized方法。
此外,不同物件例項的synchronized方法是不相干預的。也就是說,其它執行緒可以同時訪問此類下的另一個物件例項中的synchronized方法;
public synchronized void method(){ // TODO } public void method(){ synchronized(this) { // TODO } } 複製程式碼
2、某個類
此作用域下,可以防止多個執行緒同時訪問這個類中的synchronized方法。也就是說此種修飾,可以對此類的所有物件例項起作用。
public void method() { synchronized(ClassName.class) { // todo } } public static synchronized method(){ // TODO } 複製程式碼
MDove:注意一點,synchronized關鍵字是不能繼承的,也就是說,基類的方法synchronized fun(){} 在繼承類中並不自動是synchronized fun(){},而是變成了fun(){}。繼承時,需要顯式的指定它的某個方法為synchronized方法。有機會你可以自己寫個demo試一下。
常見錯誤
MDove:你來看一看下面這個demo,有沒有什麼問題?
public class ErrorSyncInstance implements Runnable{ static int i=0; public synchronized void add(){ i++; } @Override public void run() { for(int j=0;j<1000000;j++){ add(); } } public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(new ErrorSyncInstance()); Thread t2=new Thread(new ErrorSyncInstance()); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } } 複製程式碼
小A:沒覺得有問題吶?這不就是第一種加鎖的方式,鎖例項物件麼?
MDove:既然你都知道是鎖例項物件,那你沒看出來問題麼?雖然我們使用synchronized修飾了add()。但是卻new了兩個不同的例項物件,這也就意味著存在著兩個不同的例項物件鎖,因此t1和t2都會進入各自的物件鎖,也就是說t1和t2執行緒使用的是不同的鎖,因此執行緒安全是無法保證的。
小A:對對對,沒錯。那解決這種問題,是不是需要用第二種加鎖的方式,鎖住這個類?
MDove:沒錯,解決這種困境的的方式是將synchronized作用於靜態的add方法,這樣的話,物件鎖就當前類,因為類物件只有一個,因此無論new多少個例項物件都是安全的:
小A:那是不是這樣改寫就可以了?
public static synchronized void add(){ i++; } 複製程式碼
MDove:沒錯就是這樣,很簡單。接下來讓我們看一些深入的內容,鎖的實現。
synchronized鎖的底層實現
MDove:我們都知道,物件被建立在堆中。並且物件在記憶體中的儲存佈局方式可以分為3塊區域:物件頭、例項資料、對齊填充。其中物件頭,便是我們今天的主角。

關於例項資料、對齊填充的作用,各位小夥伴可以參考《深入理解Java虛擬機器》。
MDove:對於物件頭來說,主要是包括倆部分資訊:
- 1、自身執行時的資料,比如:鎖狀態標誌、執行緒持有的鎖...等等。(此部分內容被稱之為Mark Word)
儲存內容 | 標誌位 | 狀態 |
---|---|---|
物件雜湊碼、物件分代年齡 | 01 | 未鎖定 |
指向鎖記錄的指標 | 00 | 輕量級鎖定 |
指向重量級鎖的指標 | 10 | 重量級鎖定 |
空 | 11 | GC標記 |
偏向執行緒ID、偏向時間戳、物件分代年齡 | 01 | 可偏向 |
今天我們只聊: 指向重量級鎖的指標
- 2、另一部分是型別指標:JVM通過這個指標來確定這個物件是哪個類的例項。
MDove:今天我們主要聊的是物件頭,第一部分中 重量級鎖 的內容。
MDove:先讓我們從巨集觀的角度看一看synchronized鎖的實現原理。
synchronized鎖的巨集觀實現
MDove:synchronized的物件鎖,其指標指向的是一個monitor物件(由C++實現)的起始地址。每個物件例項都會有一個 monitor。其中monitor可以與物件一起建立、銷燬;亦或者當執行緒試圖獲取物件鎖時自動生成。
monitor是由ObjectMonitor實現(ObjectMonitor.hpp檔案,C++實現的),對於我們來說主要關注的是如下程式碼:
ObjectMonitor() { // 省略部分變數 _count= 0; _owner= NULL; _WaitSet= NULL; _WaitSetLock= 0 ; _EntryList= NULL ; } 複製程式碼
MDove:我們可以看到這裡定義了_WaitSet 和 _EntryList倆個佇列,其中_WaitSet 用來儲存每個等待鎖的執行緒物件。
小A:那_EntryList呢?
MDove:彆著急,讓我們先看一下_owner,它指向持有ObjectMonitor物件的執行緒。當多個執行緒同時訪問一段同步程式碼時,會先存放到 _EntryList 集合中,接下來當執行緒獲取到物件的monitor時,就會把_owner變數設定為當前執行緒。同時count變數+1。如果執行緒呼叫wait() 方法,就會釋放當前持有的monitor,那麼_owner變數就會被置為null,同時_count減1,並且該執行緒進入 WaitSet集合中,等待下一次被喚醒。
MDove:當然,若當前執行緒順利執行完方法,也將釋放monitor,重走一遍剛才的內容,也就是_owner變數就會被置為null,同時_count減1,並且該執行緒進入 WaitSet集合中,等待下一次被喚醒。
因為這個鎖物件存放在物件本身,也就是為什麼Java中任意物件可以作為鎖的原因。
synchronized程式碼塊的底層實現
MDove:咱們先寫一個簡單的demo,然後看一下它們的位元組碼:
private int i = 0; public void fun() { synchronized (this) { i++; } } 複製程式碼

MDove:根據虛擬機器規範要求,在執行monitorenter指令時,首先要嘗試獲取物件鎖,也就是上文我們提到了monitor物件。如果這個物件沒有被鎖定,或者當前執行緒已經擁有了這個物件的鎖,那麼就把鎖的計數器(_count)加1。當然與之對應執行monitorexit指令時,鎖的計數器(_count)也會減1。
MDove:如果當前執行緒獲取鎖失敗,那麼就會被阻塞住,進入_WaitSet 中,等待鎖被釋放為止。
小A:等等,我看到位元組碼中,有倆個monitorexit指令,這是為什麼呢?
MDove:是這樣的,編譯器需要確保方法中呼叫過的每條monitorenter指令都要執行對應的monitorexit 指令。為了保證在方法異常時,monitorenter和monitorexit指令也能正常配對執行,編譯器會自動產生一個異常處理器,它的目的就是用來執行 異常的monitorexit指令。而位元組碼中多出的monitorexit指令,就是異常結束時,被執行用來釋放monitor的。
小A:我們剛才看的是同步程式碼塊的原理,那麼直接修飾在方法上呢?也是通過這個倆個指令嗎?
MDove:你別說,還真不是:
public synchronized void fun() { i++; } 複製程式碼

MDove:可以看到:位元組碼中並沒有monitorenter指令和monitorexit指令,取得代之的是ACC_SYNCHRONIZED標識,JVM通過ACC_SYNCHRONIZED標識,就可以知道這是一個需要同步的方法,進而執行上述同步的過程,也就是_count加1,這些過程。
小A:哦,原來是這樣。一個是用了指令,一個是用的標識呀~對了,我聽說synchronized的效能特別低是這樣麼?
MDove:這句話不全對,JDK1.5後對synchronized進行了大刀闊斧的優化,這其中涉及到偏向鎖、輕量級鎖、自旋鎖、鎖消除等手段。時候也不早了,這些內容今天就不展開了。有機會我們下次再學習吧~