天天看點

JVM内部細節之一:synchronized關鍵字及實作細節(輕量級鎖Lightweight Locking)

  在C程式代碼中我們可以利用作業系統提供的互斥鎖來實作同步塊的互斥通路及線程的阻塞及喚醒等工作。然而在Java中除了提供Lock API外還在文法層面上提供了synchronized關鍵字來實作互斥同步原語。那麼到底在JVM内部是怎麼實作synchronized關鍵子的呢?

一、synchronized的位元組碼表示:

      在java語言中存在兩種内建的synchronized文法:1、synchronized語句;2、synchronized方法。對于synchronized語句當Java源代碼被javac編譯成bytecode的時候,會在同步塊的入口位置和退出位置分别插入monitorenter和monitorexit位元組碼指令。而synchronized方法則會被翻譯成普通的方法調用和傳回指令如:invokevirtual、areturn指令,在VM位元組碼層面并沒有任何特别的指令來實作被synchronized修飾的方法,而是在Class檔案的方法表中将該方法的access_flags字段中的synchronized标志位置1,表示該方法是同步方法并使用調用該方法的對象或該方法所屬的Class在JVM的内部對象表示Klass做為鎖對象。

二、JVM中鎖的優化:

      簡單來說在JVM中monitorenter和monitorexit位元組碼依賴于底層的作業系統的Mutex Lock來實作的,但是由于使用Mutex Lock需要将目前線程挂起并從使用者态切換到核心态來執行,這種切換的代價是非常昂貴的;然而在現實中的大部分情況下,同步方法是運作在單線程環境(無鎖競争環境)如果每次都調用Mutex Lock那麼将嚴重的影響程式的性能。不過在jdk1.6中對鎖的實作引入了大量的優化,如鎖粗化(Lock Coarsening)、鎖消除(Lock Elimination)、輕量級鎖(Lightweight Locking)、偏向鎖(Biased Locking)、适應性自旋(Adaptive Spinning)等技術來減少鎖操作的開銷。

鎖粗化(Lock Coarsening):也就是減少不必要的緊連在一起的unlock,lock操作,将多個連續的鎖擴充成一個範圍更大的鎖。

鎖消除(Lock Elimination):通過運作時JIT編譯器的逃逸分析來消除一些沒有在目前同步塊以外被其他線程共享的資料的鎖保護,通過逃逸分析也可以線上程本地Stack上進行對象空間的配置設定(同時還可以減少Heap上的垃圾收集開銷)。

輕量級鎖(Lightweight Locking):這種鎖實作的背後基于這樣一種假設,即在真實的情況下我們程式中的大部分同步代碼一般都處于無鎖競争狀态(即單線程執行環境),在無鎖競争的情況下完全可以避免調用作業系統層面的重量級互斥鎖,取而代之的是在monitorenter和monitorexit中隻需要依靠一條CAS原子指令就可以完成鎖的擷取及釋放。當存在鎖競争的情況下,執行CAS指令失敗的線程将調用作業系統互斥鎖進入到阻塞狀态,當鎖被釋放的時候被喚醒(具體處理步驟下面詳細讨論)。

偏向鎖(Biased Locking):是為了在無鎖競争的情況下避免在鎖擷取過程中執行不必要的CAS原子指令,因為CAS原子指令雖然相對于重量級鎖來說開銷比較小但還是存在非常可觀的本地延遲(可參考這篇文章)。

适應性自旋(Adaptive Spinning):當線程在擷取輕量級鎖的過程中執行CAS操作失敗時,在進入與monitor相關聯的作業系統重量級鎖(mutex semaphore)前會進入忙等待(Spinning)然後再次嘗試,當嘗試一定的次數後如果仍然沒有成功則調用與該monitor關聯的semaphore(即互斥鎖)進入到阻塞狀态。

三、對象頭(Object Header):

JVM内部細節之一:synchronized關鍵字及實作細節(輕量級鎖Lightweight Locking)

在JVM中建立對象時會在對象前面加上兩個字大小的對象頭,在32位機器上一個字為32bit,根據不同的狀态位Mark World中存放不同的内容,如上圖所示在輕量級鎖中,Mark Word被分成兩部分,剛開始時LockWord為被設定為HashCode、最低三位表示LockWord所處的狀态,初始狀态為001表示無鎖狀态。Klass ptr指向Class位元組碼在虛拟機内部的對象表示的位址。Fields表示連續的對象執行個體字段。

四、Monitor Record:

   Monitor Record是線程私有的資料結構,每一個線程都有一個可用monitor record清單,同時還有一個全局的可用清單;那麼這些monitor record有什麼用呢?每一個被鎖住的對象都會和一個monitor record關聯(對象頭中的LockWord指向monitor record的起始位址,由于這個位址是8byte對齊的是以LockWord的最低三位可以用來作為狀态位),同時monitor record中有一個Owner字段存放擁有該鎖的線程的唯一辨別,表示該鎖被這個線程占用。如下圖所示為Monitor Record的内部結構:

JVM内部細節之一:synchronized關鍵字及實作細節(輕量級鎖Lightweight Locking)

Owner:初始時為NULL表示目前沒有任何線程擁有該monitor record,當線程成功擁有該鎖後儲存線程唯一辨別,當鎖被釋放時又設定為NULL;

EntryQ:關聯一個系統互斥鎖(semaphore),阻塞所有試圖鎖住monitor record失敗的線程。

RcThis:表示blocked或waiting在該monitor record上的所有線程的個數。

Nest:用來實作重入鎖的計數。

HashCode:儲存從對象頭拷貝過來的HashCode值(可能還包含GC age)。

Candidate:用來避免不必要的阻塞或等待線程喚醒,因為每一次隻有一個線程能夠成功擁有鎖,如果每次前一個釋放鎖的線程喚醒所有正在阻塞或等待的線程,會引起不必要的上下文切換(從阻塞到就緒然後因為競争鎖失敗又被阻塞)進而導緻性能嚴重下降。Candidate隻有兩種可能的值0表示沒有需要喚醒的線程1表示要喚醒一個繼任線程來競争鎖。

五、輕量級鎖具體實作:

     一個線程能夠通過兩種方式鎖住一個對象:1、通過膨脹一個處于無鎖狀态(狀态位001)的對象獲得該對象的鎖;2、對象已經處于膨脹狀态(狀态位00)但LockWord指向的monitor record的Owner字段為NULL,則可以直接通過CAS原子指令嘗試将Owner設定為自己的辨別來獲得鎖。

擷取鎖(monitorenter)的大概過程如下:

(1)當對象處于無鎖狀态時(RecordWord值為HashCode,狀态位為001),線程首先從自己的可用moniter record清單中取得一個空閑的moniter record,初始Nest和Owner值分别被預先設定為1和該線程自己的辨別,一旦monitor record準備好然後我們通過CAS原子指令安裝該monitor record的起始位址到對象頭的LockWord字段來膨脹(原文為inflate,我覺得之是以叫inflate主要是由于當對象被膨脹後擴充了對象的大小;為了空間效率,将monitor record結構從對象頭中抽出去,當需要的時候才将該結構attach到對象上,但是和這篇Paper有點互相沖突,兩種實作方式稍微有點不同)該對象,如果存在其他線程競争鎖的情況而調用CAS失敗,則隻需要簡單的回到monitorenter重新開始擷取鎖的過程即可。

(2)對象已經被膨脹同時Owner中儲存的線程辨別為擷取鎖的線程自己,這就是重入(reentrant)鎖的情況,隻需要簡單的将Nest加1即可。不需要任何原子操作,效率非常高。

(3)對象已膨脹但Owner的值為NULL,當一個鎖上存在阻塞或等待的線程同時鎖的前一個擁有者剛釋放鎖時會出現這種狀态,此時多個線程通過CAS原子指令在多線程競争狀态下試圖将Owner設定為自己的辨別來獲得鎖,競争失敗的線程在則會進入到第四種情況(4)的執行路徑。

(4)對象處于膨脹狀态同時Owner不為NULL(被鎖住),在調用作業系統的重量級的互斥鎖之前先自旋一定的次數,當達到一定的次數時如果仍然沒有成功獲得鎖,則開始準備進入阻塞狀态,首先将rfThis的值原子性的加1,由于在加1的過程中可能會被其他線程破壞Object和monitor record之間的關聯,是以在原子性加1後需要再進行一次比較以確定LockWord的值沒有被改變,當發現被改變後則要重新進行monitorenter過程。同時再一次觀察Owner是否為NULL,如果是則調用CAS參與競争鎖,鎖競争失敗則進入到阻塞狀态。

釋放鎖(monitorexit)的大概過程如下:

(1)首先檢查該對象是否處于膨脹狀态并且該線程是這個鎖的擁有者,如果發現不對則抛出異常;

(2)檢查Nest字段是否大于1,如果大于1則簡單的将Nest減1并繼續擁有鎖,如果等于1,則進入到第(3)步;

(3)檢查rfThis是否大于0,設定Owner為NULL然後喚醒一個正在阻塞或等待的線程再一次試圖擷取鎖,如果等于0則進入到第(4)步

(4)縮小(deflate)一個對象,通過将對象的LockWord置換回原來的HashCode值來解除和monitor record之間的關聯來釋放鎖,同時将monitor record放回到線程是有的可用monitor record清單。

六、參考資料:

周志明的《深入了解Java虛拟機》

部落格https://blogs.oracle.com/dave/entry/biased_locking_in_hotspot

David Dice's paper Implementing Fast Java Monitors with Relaxed-Locks

部落格http://www.javaworld.com/article/2076971/java-concurrency/how-the-java-virtual-machine-performs-thread-synchronization.html

注:有了解錯誤之處歡迎指出,謝謝!