虛擬機中對象鎖實現分析

一、前言

編程過程中經常會遇到線程的同步問題,Java 中對同步問題的解決方案比較多(synchronized、JUC、原子操作、volatile、條件變量等),其中 synchronized 最方便、簡單易用,也是 java 編程中使用最多的臨界區保護方案。本文主要講述對象鎖的相關知識,詳細介紹 synchronized 和 Object 的關鍵方法的虛擬機實現原理。

二、Java 對象鎖的使用方式

2.1 實例方法的同步

圖片

synchronized 修飾實例方法,該同步僅對當前對象的該方法起作用,同一時間只能有一個線程可以進入該對象的此方法。對於不同對象的此函數,無法做到互斥保護。

2.2 靜態方法的同步

圖片

synchronized 修飾靜態方法,該同步對當前類對象的該方法起作用,同一時間只能有一個線程可以進入該方法。

2.3 代碼塊的同步

圖片

在大多少情況下,並不需要對整個方法進行保護,當 synchronized 修飾代碼塊時,該代碼塊的訪問依賴於 object 對象鎖的互斥訪問,同一時間只能有一個線程持有 object 對象鎖。

更準確的來講,synchronized 關鍵字是依賴於對象鎖而生效的,每個 synchronized 同步塊開始的地方都會生成 monitor-enter obj 指令,同步塊結束的地方生成 monitor-exit obj 的指令,其中 obj 爲用於控制互斥訪問的對象。同一時間只能有一個線程持有 obj 的對象鎖。在 2.1 中 synchronized 依賴的是實列對象,2.2 中 synchronized 依賴的是類對象,2.3 中 synchronized 依賴的是 object 對象。

當一個對象控制多個代碼塊時,多個代碼塊也是互斥訪問,如下面代碼:

圖片

代碼塊①和代碼塊② 雖然在兩個函數中,但是 synchronized 依賴的對象都爲 object,這兩個代碼塊也是互斥訪問。

2.4 Object  wait() 和 notify() 使用方法

Object 作爲所有類的基類,都實現了 object 方法。典型的用法如下:

圖片

thread 1 持有 object 對象鎖,並調用 object.wait() 方法後,則該線程進入 WAITING 狀態,並釋放 object 對象鎖,等待其它線程來喚醒它。

當 thread 2 持有 object 對象鎖,並調用 object.notify() 方法後,喚醒 thread 1,thread 1

重新獲得 object 對象鎖繼續執行。Object 類方法說明:

圖片

三、Android 對象內存結構

3.1 對象內存結構

圖片

一個類的實例對象內存主要由 3 部分組成:

1). 對象頭:對象頭包括 kclass_和 monitor_兩個字段,其中 kclass_ 存放指向類對象的指針,通過該指針可以找到該對象對應的類,monitor_ 用於存放對象運行時的標識數據,例如: GC 標誌位、哈希碼、鎖狀態等信息, 後面詳細分析。

2). 實例數據,該部分存放實例變量值,父類實例變量值在前,子類在後,且實例變量值按照如下順序進行排序:

圖片

3). 對齊填充,對象在內存中是按照 8byte 對齊的,如果實例數據部分沒有按照 8byte 對齊,則填充爲 8byte 對齊。

**3.2 monitor_ 字段分析**

monitor_ 字段定義在 art/runtime/mirror/object.h,類型爲 uint32_t,主要有下面 3 個操作函數。

圖片

操作函數中 SetLockWord 和 CasLockWord 函數的入參或 GetLockWord 函數的返回值都包含 LockWord 變量,對 monitor_ 字段的操作是通過 LockWord 的值進行的。

下面再來看 LockWord 定義:

LockWord 類的定義在 art/runtime/lock_word.h 文件中,從註釋中可以看到 LockWord 的使用主要有 4 種狀態,如下:

圖片

LockWord 的設計非常精妙,一個 32 位數據的每一位都充分利用,而且很好的區分了不同狀態。下面對各狀態進行詳細說明:

  1. unlocked/thin 狀態下 31-30 bit 爲 00,默認狀態下爲 unlocked 狀態,當對象進行線程同步時變成 thin lock 狀態,27-16bit 記錄了 thin lock 重入的次數,15-0 bit 記錄了持有該 thin lock 的線程 ID。

  2. fat lock 狀態下 31-30 bit 爲 01,當對象鎖在 thin lock 狀態,且有新的 (非 owner) 線程與其競爭,經過適當的等待期(sched_yield 調用、循環獲取 thin lock 狀態)後依然無法拿到鎖,則轉換爲 fat lock 狀態,併爲該對象分配一個 Monitor 資源。

  3. hash state 狀態下 31-30 bit 爲 10,在 27-0 bit 存儲對象的 hash code,當在其它模式下,hash code 會存儲在該對象關聯的 Monitor 對象中。

  4. forwarding address state 狀態下 31-30 bit 爲 11,在 concurrent copying GC 的 copy 階段,當一個對象被拷貝後,指向拷貝後的對象地址,當線程訪問到該對象後,通過該轉發地址,訪問新的對象。

第 29 位爲 mark bit,通過該 bit 位可以快速判斷是否標記過,避免重複標記。

第 28 位爲 read barrier bit,如果對象 LockWorkd 的該 bit 被設置,則在訪問該對象的成員時會進入慢速路徑,判斷對象是不是需要更新,如果需要更新,則返回拷貝後的對象地址。

四、對象鎖代碼分析

4.1 首先我們看一段代碼

圖片

這段代碼比較簡單,主要有下面兩個核心點:

1). 在主線程執行的過程中,用 obj 對象進行線程同步,並調用 obj.wait() 函數,使線程阻塞在了 obj 對象鎖上等待喚醒。

2). main 函數中創建匿名線程,該線程首先 sleep 2000ms,然後喚醒阻塞在 obj 對象鎖上線程。

4.2 編譯 TestDemo.java, 命令如下:

圖片

1).Javac 將 TestDemo.java 文件編譯生成 TestDemo*.class 文件, java 編譯過程中每個類會生成一個 class 文件。

2).d8 命令將 TestDemo*.class 文件通過編譯、重構、重排、壓縮、混淆後生成對應的 dex (Dalvik Executable file)格式文件。

3).dexdump.exe 命令可以查看 dex 文件格式的詳細信息,如校驗信息、dex 頭信息、生成 dex 的 CFG 信息、dex 的反彙編信息等,詳細使用方法可以通過 dexdump.exe –help 命令查看

通過 dexdump.exe –d classes.dex 查看反彙編

其中 run 方法指令信息如下:

圖片

main 函數的指令信息如下:

圖片

對部分指令解析如下:

圖片

對於 dex 指令詳細格式可以閱讀 google 的官方文檔:

https://source.android.com/docs/core/runtime/dalvik-bytecode

本文重點分析 monitor-enter、monitor-exit、Object.wait()、Object.notify() 在虛擬機中的詳細實現。

4.3.Object.wait() 流程分析

Object.wait() 的調用關係如下:

圖片

Object 類是所有類的父類,任何類中都可以調用 public 的 wait() 方法,最終調用到虛擬機的 monitor.cc 文件的 wait 靜態方法,

圖片

首先構造了一個操作 obj 的 Handle 對象 h_obj,通過 ObjectWaitStart 函數通知 jvmti 調試系統發生了 JVMTI_EVENT_MONITOR_WAIT 事件。

JVMTI(JVM Tool Interface)是 Java 虛擬機所提供的 native 編程接口,可以用來開發並監控虛擬機,可以查看 JVM 內部的狀態,並控制 JVM 應用程序的執行。可實現的功能包括但不限於:調試、監控、線程分析、覆蓋率分析工具等。

圖片

首先獲得 h_obj 對象的 LockWord 字段,lock_word.GetState() 函數獲得當前的鎖狀態,主要有下面幾種情況:

1).hash 或 unlocked 狀態:

因爲調用 wait() 方法必須持有對象鎖,所以不會出現這兩種狀態,如果出現則拋出 IllegalMonitorStateException 異常。

2).thin lock 狀態:

當持有該對象鎖的線程不是要 wait 的線程,也拋出 IllegalMonitorStateException 異常,當持有鎖的線程與要 wait 的線程一致,這時需要將 thin lock inflate 爲 fat lock,inflate 的過程在 monitor-enter 指令分析中分析。

當對象鎖 inflate 爲 fat lock 狀態後,調用 Monitor 對象的實例方法 Wait 讓線程進入 sleep 狀態等待。

4.4 Object.notify() 流程分析

圖片

這裏我們直接分析 DoNotify 函數:

圖片

通過 lock_word.GetState() 獲得當前 obj 對象的鎖狀態,主要有下面情況:

  1. hash 或 unlocked 狀態 :

拋出 IllegalMonitorStateException 異常。

2).thin lock 狀態:

當持有該對象鎖的線程不是要 notify 的線程,也拋出 IllegalMonitorStateException 異常,當持有鎖的線程與要 notify 的線程一致,這時說明沒有需要通知喚醒的線程,直接返回。

3).fat lock 狀態:

在 Object.notify() 流程中參數 notify_all 爲 false,則直接調用 mon->Notify(self); 通知喚醒等待線程。

4.5monitor-enter 流程分析

對於解釋執行和機器碼執行模式,最終都會調用到 art/runtime/mirror/object-inl.h 文件 Object 對象的 MonitorEnter 函數。

圖片

下面來分析 Monitor 類的靜態方法 MonitorEnter 函數。

圖片

FakeLock 主要用於線程安全性檢查,主要在編譯期檢測。

kExtraSpinIters 定義了當對象鎖被其它線程持有且爲 thin lock 時,競爭線程循環獲取鎖的次數。

圖片

通過 lock_word.GetState() 獲取鎖狀態,當鎖狀態爲 unlocked 狀態時,轉換爲 thin lock 狀態,並通過 cas 操作更新 lock count。

圖片

當鎖狀態爲 thin lock 狀態時,首先獲取鎖的 owner 線程 id,如果 owner id 與競爭線程 id 一致,則有下面兩種情況:

  1. 如果 lock count 加 1 小於等於 (1<<12)-1(4095) 時,將 lock count+1 更新 lock count。

  2. 如果 lock count 加 1 大於 (1<<12)-1 時 (lock count 區域無法存儲),則調用 InflateThinLocked 函數對 thin lock 進行膨脹。

Atrace* 相關的函數主要用於 systrace 相關信息的打印,trylock 在這裏爲 false。

圖片

當鎖狀態爲 thin lock 狀態且鎖的 owner 線程 id 與競爭線程 id 不一致,則做一定的等待。

runtime->GetMaxSpinsBeforeThinLockInflation() 的值爲 50 ,也就是說執行 100 次的循環判斷鎖狀態後,再執行 50 次的 sched_yield() 後還未獲得鎖資源,如果還未拿到鎖,則對該鎖進行膨脹。sched_yield() 會主動讓出當前線程的執行權限,並在某個時間後恢復執行。

圖片

當鎖狀態已經是 fat lock 狀態,通過 lock_word.FatLockMonitor(); 獲取 Monitor 對象,並通過 Monitor 對象的 Lock 函數讓線程進入等待狀態。

圖片

當鎖狀態已經是 hash 狀態時,直接對鎖進行膨脹。

下面看鎖膨脹的過程:

圖片

thin lock 的膨脹有兩種情形:

1).lock count 的值超過了 4095,這時鎖的 owner 爲當前線程,即直接通過 Inflate 函數膨脹

2). 鎖的 owner 不是當前線程,通過 SuspendThreadByThreadId 暫停鎖的 owner 線程(主要是 owner 線程和鎖膨脹線程都需要訪問對象的 LockWord,避免競態問題),然後通過 Inflate 進行膨脹。膨脹完成後再喚醒鎖的 owner 線程。

再看 Inflate 的過程:

圖片

通過 MonitorPool::CreateMonitor 函數獲取一個 Monitor 的對象 m,並通過 m->install(self) 函數更新對象的 LockWord 字段,這時 LockWord 字段信息包含 fat lock 狀態、GC 狀態、MonitorId,然後將 m 保存在 monitor_list_ 中。

monitor_list_中存儲了當前虛擬機使用的所有 Monitor 對象。在 GC 的過程中,通過該鏈表,訪問到 Monitor 依賴的對象。如果對象變成垃圾對象,則回收該 Monitor,否則更新 Monitor 依賴的對象信息。

MonitorId 用於唯一標識一個 Monitor,生成的方法可以看 monitor_pool.h 中的實現。

再看 Monitor::Lock 的過程:

該函數的實現較長,省去調試相關的代碼。

圖片

首先介紹 Monitor 中最重要的成員 monitor_lock_ ,它是 Mutex 的實例,通過該實例實現鎖相關的核心邏輯。

TryLock 函數主要是通過 Mutex 的函數實現一定的自旋等待,並設置鎖的狀態爲線程持有的狀態。

monitor_lock_.ExclusiveLock(self); 在 Mutex 的 ExclusiveLock 函數中通過 futex 系統調用實現了線程的阻塞,futex 調用代碼如下。

圖片

4.6 monitor-exit 流程分析

解釋執行和機器碼執行模式都會調用到 MonitorExit 函數。

圖片

通過 lock_word.GetState() 獲取 LockWord 狀態,當狀態爲 hash 或 unlocked 狀態時,通過 FailedUnlock 函數拋出異常。

圖片

當 LockWord 的狀態爲 thin lock 狀態時,有下面兩種情況:

1). 鎖的 owner 與當前線程不一致,則出錯拋出異常。

2). 鎖的 owner 與當前線程爲同一線程,當鎖有重入時,則將 lock count -1,否則設置爲 unlocked 狀態。

圖片

當 LockWord 的狀態爲 fat lock 狀態時,獲取該對象關聯的 Monitor 對象,並調用 Unlock 函數

圖片

在 Unlock 函數中 lock_count 爲 0,說明該線程不在持有該鎖,通過

SignalWaiterAndReleaseMonitorLock 喚醒阻塞在該鎖上的線程。

五、總結

本文簡單的闡述了對象鎖的使用方式,對象在內存中的結構,並對對象頭中關鍵成員 LockWord 進行了分析,最後介紹了 synchronized、Object.wait() 和 Object.notify() 在虛擬機中的實現流程。

參考文獻:

深入理解 Android Java 虛擬機 ART  鄧凡平

深入理解 java 虛擬機  周志明

深入 java 虛擬機      bill venners

Android T 源碼

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/EeqCBKfQsfTEZLO7dEjvUg