概述
上篇博客我簡單介紹了 Synchronized 鎖的三種使用方法及部分特性。作爲開發者,適當的瞭解原理可以加深我們對它的理解。因此,本篇博客我打算從底層介紹一下 Synchronized 鎖實現的原理。
Synchronized 鎖實現原理
本篇博客分以下四個模塊展開:
- 對象頭
- Monitor 對象
- 顯式同步和隱式同步
- Monitor 與 阻塞喚醒
1、 對象頭
在 java 代碼中,對象創建完成後,在堆區分以下三個模塊存儲:
- 對象頭
- 實例數據
- 填充數據
其中實例數據主要保存類屬性及數據信息。JVM 規定 Java 對象的起始地址必須是8的倍數,因此填充數據不存儲任何數據,僅僅爲了字節對齊。
最後我們來看對象頭區域:一般對象頭由以下兩個模塊組成:
- Mark Word
- Class Metadata Address
Class Metadata Address 模塊包含“引用”指向 Class 類對象,通過該標識確定對象的類型。
重點是 Mark Word 模塊,該模塊記錄當前對象的hash值,gc 分代年齡以及鎖信息。這裏的鎖信息包括是否偏向鎖以及鎖標誌位。默認情況下 32 位虛擬機中未加鎖對象的 Mark Word 結構如下
對象hashCode | 對象分代年齡 | 固定爲0(加鎖狀態時表示是否偏向鎖) | 鎖標誌位 |
---|---|---|---|
25 bit | 4 bit | 1bit | 2 bit |
根據不同的鎖標誌位,我們可以確定當前鎖的狀態:無鎖、輕量級鎖、偏向鎖、重量級鎖等等。關於不同鎖的特性後面我們出博客專門介紹,本篇我們主要來看重量級鎖。
當鎖標誌爲10時,表示當前鎖狀態爲重量級鎖。此時存在指針指向該對象對應的鎖,也就是 Monitor 對象。Synchronized 鎖就是基於進入和退出 Monitor 對象實現的。下面我們具體看 Monitor 對象的數據結構。
2、Monitor 對象
Monitor 對象又叫管程對象,是 java Synchronized 鎖實現的原理。任何 java 對象都有一個管程對象和它相連,其中管程對象可以隨着對象的創建而創建,也可以在線程獲取某個對象鎖資源時創建,其中線程獲取的鎖資源就是 Monitor 管程對象的持有權。當 Monitor 對象被某個線程獲取後,該對象會處於鎖死狀態,也就是說當其他線程再來獲取當前對象的鎖資源時阻塞等待。
Monitor 是由 ObjectMonitor 實現的,該類是由 C++ 編寫的。下面我們看一下該類的核心屬性:
ObjectMonitor() {
_count = 0;
_owner = NULL;
_WaitSet = NULL;
_EntryList = NULL ;
}
在上述代碼中,_count 屬性表示當前鎖重入的次數,_owner 指向當前獲取鎖的線程。_WaitSet 和 _EntryList 都是隊列,前者保存調用 wait() 方法阻塞的線程對象,後者保存正在等待鎖的線程對象。
下面我舉一個簡單的例子:線程A 和 線程B 爭搶 對象C 的鎖
- 對象C 的管程對象初始化,所有屬性都爲 0 或 NULL
- 線程A 和 線程B 進入該管程對象的 _EntryList 隊列中
- 線程A 搶佔到鎖,從 _EntryList 隊列中出來,管程對象 _owner 指向該線程,count 值加一
- 線程A 調用 wait() 方法釋放鎖,進入管程對象的 _WaitSet 隊列,_owner 指向爲空,count 值自減一
- 線程B 搶佔到鎖,從 _EntryList 隊列中出來,管程對象 _owner 指向該線程,count 值加一
- 線程B 再次搶該鎖,count 值繼續加一
- 線程B 調用該對象的 notify() 方法,線程A 從 _WaitSet 隊列中釋放,進入 _EntryList 隊列搶鎖
- 線程B 執行完第一塊同步代碼,count 值減一
- 線程B 執行完第二塊同步代碼,count 值減一,此時_count 值爲0,_owner 指向爲空
- 線程A 搶佔到鎖,_owner 指向線程A 對象,count 值加一
- 線程A 執行完同步代碼塊,count 值減一,_owner 指向爲空,兩個線程都執行完畢
從這個簡單的過程,我們可以看出:
- _owner 標識判斷 Monitor 對象是否被佔用 以及 保證線程同步
- _count 標識確定佔有 Monitor 對象的線程是否執行完同步代碼塊需要釋放鎖
- _waitSet 標識保證調用 wait() 方法的線程被阻塞且不搶佔鎖
- _EntrySet 標識保證後來的線程阻塞等待
以上這些也就是我理解的 Monitor 對象實現線程同步的原理。
3、顯式同步和隱式同步
上面介紹了 Monitor 對象關於同步的操作,下面我們來看 synchronized 具體是怎麼實現的。
根據 synchronized 使用方法的不同,它所實現的同步有以下兩種類型:顯式同步 和 隱式同步。
-
當 synchronized 修飾代碼塊時,通過指令 monitorenter 和 monitorexit 實現同步,這種方式也叫顯示同步
-
當 synchronized 修飾方法時,根據 ACC_SYNCHRONIZED 指令判斷是否同步,這種方式稱爲隱式同步
3-1、顯示同步
下面我們看一個使用 synchronized 修飾代碼塊的簡單代碼:
public class SynTest {
int num = 0;
public void add() {
synchronized (this) {
num++;
}
}
}
我們使用 javap 反編譯上述代碼會得到以下結果:
Compiled from "SynTest.java"
public class com.xxx.worker.lock.SynTest {
int num;
public com.xxx.worker.lock.SynTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field num:I
9: return
public void add();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: dup
6: getfield #2 // Field num:I
9: iconst_1
10: iadd
11: putfield #2 // Field num: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:
from to target type
4 16 19 any
19 22 19 any
}
重點關注指令 monitorenter 和 monitorexit。其中 monitorenter 指令指向同步代碼開始的模塊,monitorexit 指向同步代碼結束的模塊。通過這兩條指令標識被 synchronized 修飾的代碼塊
當執行 monitorenter 指令時:線程試圖獲取當前對象對應 Monitor 對象的持有權。
- 如果當前線程搶鎖成功,Monitor 對象 _owner 指向當前線程,_count 加一。
- 如果當前線程已經佔有鎖,Monitor 對象 _count 加一。
- 如果 Monitor 對象已經被其他線程持有,當前線程阻塞,進入 Monitor 對象的 _entrySet隊列。
當執行 monitorexit 指令時:當前線程將 Monitor 對象的 _count 值減一。
- 如果減完之後 _count 值爲0,說明當前線程已經執行完同步代碼塊,_owner 置爲空,釋放鎖資源
- 如果減完之後 _count 值不爲0,說明當前線程還沒有執行完同步代碼塊,不會釋放鎖資源
需要注意的一點是,每一個 monitorenter 指令都會對應一個 monitorexit 指令。即使線程執行過程中拋出異常,在捕獲異常後還會執行 monitorexit 指令。通過這種方式保證鎖對象最終一定會被釋放,確保多線程不會因爲某個線程佔有鎖異常終止而無法正常運轉。
3-2、隱式同步
下面我們看一個用 synchronized 修飾實例方法的簡單案例:
public class SynTest {
int num = 0;
public synchronized void add() {
num++;
}
}
我們使用 javap 反編譯上述代碼會得到以下結果:
Compiled from "SynTest.java"
...
public synchronized void add();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field num:I
5: iconst_1
6: iadd
7: putfield #2 // Field num:I
10: return
LineNumberTable:
line 11: 0
line 12: 10
}
從編譯後的字節碼來看,synchronized 修飾方法時,並沒有使用 monitorenter 指令,而是通過從運行時常量池讀取該方法的 ACC_SYNCHRONIZED 標識,通過該標識證明當前方法是一個同步方法。當線程執行該方法時,就會進行相應的同步操作。
4、Monitor 與 阻塞喚醒
最後我們再來聊一下 wait()、notify()、notifyAll() 這三個方法。
-
之前我們提到這三個方法都需要在 synchronized 修飾的代碼塊中,否則拋出異常。
其實原因是這樣的:wait()、notify(),notifyAll() 也是基於 Monitor 管程對象實現的。調用 wait() 方法讓當前線程放棄管程對象,進入 _WaitSet 隊列,而 notify() 和 notifyAll() 方法讓當前線程喚醒 _WaitSet 隊列中的線程。也就是說,無論這三個方法中哪一個,都需要擁有當前管程對象的持有權,而 synchronized 標識保證線程獲取管程對象的持有權,這也是爲什麼這三個方法都需要在 synchronized 修飾的代碼塊中。
-
接下來我們再聊一下爲什麼這三個方法都是 Object 方法?
其實是因爲任何 java 對象都會有一個鎖對象與之關聯,也就是說任何對象都可以調用 wait() 、notify()、notifyAll() 方法,而java語言所有對象繼承 Object 類,這也是這三個方法都是 Object() 方法的主要原因。
-
最後我們來聊聊 join() 方法
之前我提到過,join() 方法底層通過調用 wait() 方法實現。也就是說,讓調用 join() 方法的線程對象進入參數線程鎖對象的 _WaitSet 隊列中。當參數線程執行完畢後,喚醒 _WaitSet 隊列中的線程,調用 join() 方法的線程隨即恢復狀態,這也是 join() 方法保證線程運行次序的原理。
這裏我暫時列舉這麼多,關於 java 線程常見方法有問題的讀者可以點擊這裏查看我之前的博客。