synchronized憑什麼鎖得住?

相關連接:html

《synchronized鎖住的是誰?》程序員

咱們知道synchronized是重量級鎖,咱們知道synchronized鎖住的是一個對象上的Monitor對象,咱們也知道synchronized用於同步代碼塊時會執行monitorenter和monitorexit等。數組

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

那麼synchronized爲何「重」呢?Monitor對象從何而來呢?synchronized用於實例方法或者靜態方法又是怎麼鎖住的呢?性能

《synchronized鎖住的是誰?》中咱們明確了,synchronized鎖住的對象,本文講述synchronized憑什麼鎖得住。優化

首先咱們須要知道的是在Hotspot虛擬機實現中,對象實例在堆內存中結構分爲3個部分:對象頭、實例數據、對其填充字節。在Java中萬物皆爲對象。就算一個Java類被編譯稱爲class二進制文件在被加載到內存時,它仍然會在堆內存中建立一個Class對象。這也就解釋了,爲何synchronized能對類加鎖(由於每一個類在堆內存中有一個Class對象,對於類synchronized鎖的其實是Class對象,下文會繼續解釋)。spa

在解釋了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)

 

相關文章
相關標籤/搜索