synchronized憑什么鎖得住?


相關鏈接:

《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)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM