synchronized憑什麼鎖得住?

OKevin發表於2019-06-19

相關連結:

《synchronized鎖住的是誰?》

我們知道synchronized是重量級鎖,我們知道synchronized鎖住的是一個物件上的Monitor物件,我們也知道synchronized用於同步程式碼塊時會執行monitorenter和monitorexit等。

上面幾個問題僅僅是校招級。

那麼synchronized為什麼“重”呢?Monitor物件從何而來呢?synchronized用於例項方法或者靜態方法又是怎麼鎖住的呢?

《synchronized鎖住的是誰?》中我們明確了,synchronized鎖住的物件,本文講述synchronized憑什麼鎖得住。

首先我們需要知道的是在Hotspot虛擬機器實現中,物件例項在堆記憶體中結構分為3個部分:物件頭、例項資料、對其填充位元組。在Java中萬物皆為物件。就算一個Java類被編譯稱為class二進位制檔案在被載入到記憶體時,它仍然會在堆記憶體中建立一個Class物件。這也就解釋了,為什麼synchronized能對類加鎖(因為每個類在堆記憶體中有一個Class物件,對於類synchronized鎖的實際上是Class物件,下文會繼續解釋)。

在解釋了Java中物件例項在Hotspot中的記憶體結構(物件頭、例項資料、對其填充位元組)後,synchronized鎖住的Monitor物件就存在於物件頭之中。物件頭又分為:Mark Word、指向類的指標、陣列長度(陣列物件)。

物件頭在Hotspot虛擬機器實現中,分為32位和64位的實現,實際上Hotspot原始碼實現中的註釋已經解釋得非常清楚了(openjdk/hotspot/share/oops/markOop.hpp),物件頭的Mark Word位格式在32位機器中是32位長,在64位機器中是64位長(採用 big endian ,低地址存放最高有效位元組,即低位在左,高位再右)。

32bit位虛擬機器Mark Word

鎖狀態

25bit

4bit

1bit

2bit

23bit

2bit

是否是偏向鎖

鎖標誌位

無鎖狀態

物件的hashcode

分代年齡

0

01

偏向鎖

執行緒ID

偏向時間戳

分代年齡

1

01

輕量級鎖

指向棧中鎖記錄的指標

00

重量級鎖

指向重量級鎖(Monitor)的指標

10

GC標記

11

和synchronized相關的就是Java在Hotspot虛擬機器實現中物件頭中的Mark Word。

在以前(JDK5之前),synchronized被稱為重量級鎖是無可厚非的,但在JDK6後,JVM對其進行了一系列優化,儘量使得synchronized不再那麼重。之所以synchronized重,是因為它涉及到了作業系統使用者態與核心態的轉換,下文再詳細解釋。這裡我們從最輕的偏向鎖->輕量級鎖->重量級鎖的過程,注意他們只能升級加鎖的強度,不能降級。

偏向鎖

上面提到了JDK6過後優化了synchronized的加鎖過程,儘量使得synchronized不再那麼重。偏向鎖即是如此。

JVM的研究者表明,大多數情況下鎖的競爭不是那麼激勵,在不那麼激勵的時候如果通過獲取Monitor來進行同步訪問,會造成執行緒在作業系統使用者態和核心態的轉換,這會使得系統效能下降。偏向鎖表示,當只有一個執行緒進入同步方法或同步程式碼塊時,並不會直接獲取Monitor鎖,而是先判斷物件頭中Mark Word部分的鎖標誌位是否處於“01”,如果處於“01”,此時再判斷執行緒ID是否是本執行緒ID,如果是則直接進入方法進行後續操作;如果不是,此時則通過CAS(無鎖機制競爭)如果競爭成功,此時將執行緒ID設定為本執行緒ID,如果競爭失敗,說明造成了有了較為強烈的鎖競爭,偏向鎖已不能滿足,此時偏向鎖晉級為輕量級鎖。

輕量級鎖

當鎖發生競爭時,持有偏向鎖的執行緒會撤銷偏向鎖,轉而晉級為輕量級鎖(狀態)。輕量級鎖的核心是,不讓未獲取鎖的執行緒進入阻塞狀態,因為這會使得執行緒由使用者態轉為核心態,這會造成很大的效能損失,而是採用“死迴圈”的方式不斷的獲取鎖,這種採用“死迴圈”獲取的鎖的方式稱為——鎖自旋。它不會讓執行緒陷入阻塞,但同時僅適用於持有鎖時間較短的場景。那麼輕量級鎖升級為重量級鎖的條件就是,自旋等待的時間過長,並且又有了新的執行緒來競爭。

重量級鎖

這種鎖,就是地地道道原原本本synchronized的本意了。執行緒會去搶奪物件上的一個互斥量(這個互斥量就是Monitor),每個物件都會有,就算是類也有一個Monitor互斥量(因為類在堆記憶體中有一個Class物件)。當一個執行緒獲取到物件的Monitor鎖時,其餘執行緒會被阻塞掛起,並且由使用者態轉為核心態。

上文提到在鎖的競爭狀態晉級為重量級鎖時,Java物件頭中的Mark Word前30位儲存的是Monitor物件的指標。Monitor物件定義在openjdk/hotspot/share/runtime/objectMonitor.hpp中,在ObjectMonitor中定義了:計數器、持有Monitor的執行緒、處於wait狀態的執行緒、處於阻塞狀態的執行緒等等。

synchronized無論是普通例項還是同步程式碼塊,它所獲取的鎖是物件例項中的Monitor鎖,而物件的Monitor又是存在於Java物件頭的Mark Work之中,所以可以這麼說,synchronized獲取的鎖在Java物件頭中。對於普通例項或者靜態方法,JVM並沒有顯示的指令進入臨界區,而是在方法上標識了“ACC_SYNCHRONIZED”,標識是synchronized同步方法,方法內部都是臨界區。而對於同步程式碼塊,則在synchronized程式碼塊開始執行了monitorenter,結束或者丟擲異常時執行了monitorexit指令。

synchronized憑藉的就是Monitor鎖住的物件,Monitor又是藉助於作業系統的mutex lock,之所以它重是因為它被掛起後執行緒會由使用者態轉換為核心態,這個轉換會帶來效能損耗。JDK6開始對其進行了優化,提出了偏向鎖和輕量級鎖,針對鎖競爭較為激烈的場景不會直接去獲取Monitor物件,減少效能損耗。因此在現如今的synchronized實現中,它的效能劣勢也已不再那麼明顯。

 

 

這是一個能給程式設計師加buff的公眾號 (CoderBuff)

 

相關文章