並發編程學習筆記02-Java並發機制的底層原理之synchronized
- 2020 年 1 月 22 日
- 筆記
該並發學習系列以閱讀《Java並發編程的藝術》一書的筆記為藍本,彙集一些閱讀過程中找到的解惑資料而成。這是一個邊看邊寫的系列,有興趣的也可以先自行購買此書學習。
synchronized實現同步的基礎:Java中的每一個對象都可以作為鎖。具體表現為三種形式:
- 對於普通同步方法,鎖是當前實例對象。
- 對於靜態同步方法,鎖是當前類的Class對象。
- 對於同步方法塊,鎖是synchronized括弧里配置的對象。
當一個執行緒試圖訪問同步程式碼塊時,它首先必須得到鎖,退出或拋出異常是必須釋放鎖。
JVM基於進入和退出Monitor對象來實現方法同步和程式碼塊同步,兩者實現細節不同,但都可使用 monitoreneter
和 monitorexit
這兩個指令來實現。
- monitorenter指令是在編譯後插入到同步塊的開始位置。
- monitorexit指令是插入到方法結束處和異常處。
- JVM要保證每個monitorenter必須有對應的monitorexit與之配對。
- 任何對象都有一個monitor與之關聯,且當一個monitor被持有後,它將處於鎖定狀態。
- 程式執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象的鎖。
Java對象頭
synchronized用的鎖是存在Java對象頭里的。
- 若對象是數組類型,則虛擬機用3字寬(Word)存儲對象頭;
- 若對象是非數組類型,則用2字寬存儲對象頭。
在32位虛擬機中,1字寬等於4位元組,即32bit。依次,在64位虛擬機中,1子寬為64bit。
Java對象頭組成如下:
長度 |
內容 |
說明 |
備註 |
---|---|---|---|
32/64 bit |
Mark Word |
存儲對象的hashCode或鎖資訊等 |
默認存儲對象的HashCode、分代年齡和鎖標記位。 |
32/64 bit |
Class Metadata Address |
存儲對象的類型數據的指針 |
該指針指向對象的類元數據,JVM通過這個指針確定對象是哪個類的實例。 |
32/64 bit |
Array Length |
數組的長度(如果當前對象是數組) |
j當且僅當對象是數組時才會有這部分。 |
32位JVM的Mark Word的默認存儲結構如下:
鎖狀態 |
25bit |
4bit |
1bit 是否是偏向鎖 |
2bit 鎖標記位 |
---|---|---|---|---|
無鎖狀態 |
對象的HashCode |
對象的分代年齡 |
0 |
01 |
運行期間,Mark Word 里存儲的數據會隨著鎖標誌位的變化而變化。可能會變成存儲以下4種數據:

在64位虛擬機下,Mark Word是64bit大小的,其存儲結構如下所示:

由此可見無鎖狀態和偏向鎖狀態時鎖標誌位均是01,只是在前面的1bit區域區分了當前是無鎖狀態還是偏向鎖狀態。
UseCompressedOops
64位的JVM將會比32位的JVM多耗費50%的記憶體。
從JDK 1.6 update14開始,64 bit JVM正式支援了
-XX:+UseCompressedOops
這個可以壓縮指針,起到節約記憶體佔用的新參數。
oop即ordinary object pointer普通對象指針。開啟該選項後,下列指針將壓縮至32位:
- 1.每個Class的屬性指針(即靜態變數)
- 2.每個對象的屬性指針(即對象變數)
- 3.普通對象數組的每個元素指針
當然不是所有指針都被壓縮,一些特殊類型的指針JVM不會優化,比如:指向PermGen的Class對象指針(JDK8中指向元空間的Class對象指針)、本地變數、堆棧元素、入參、返回值和NULL指針等。
被壓縮的還有數組對象頭的Array Length 部分。
該指令原理是:解釋器在解釋位元組碼時,植入壓縮指令。
關於JVM中的對象相關內容可查看:JVM-HotSpot虛擬機對象探秘 。
鎖的升級與對比
Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」,以及鎖升級的概念。
JDK1.6中,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。
這幾個狀態會隨著競爭情況逐漸升級。鎖可以升級但不能降級。這種策略的目的是為了提高獲得鎖和釋放鎖的效率。
幾種鎖的優缺點對比如下:
鎖 |
優點 |
缺點 |
適用場景 |
---|---|---|---|
偏向鎖 |
加鎖和解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級的差距。 |
如果執行緒間存在鎖競爭,會帶來額外的鎖撤銷的消耗。 |
適用於只有一個執行緒訪問同步塊的場景。 |
輕量級鎖 |
競爭的執行緒不會阻塞,提高了程式的響應速度。 |
如果始終得不到鎖競爭的執行緒,使用自旋會消耗CPU。 |
追求響應時間;同步塊執行速度非常快。 |
重量級鎖 |
執行緒競爭不使用自旋,不會消耗CPU。 |
執行緒阻塞,響應時間緩慢。 |
追求吞吐量,同步塊執行速度較長 |
偏向鎖
大多數情況下鎖不僅不存在多執行緒競爭,而且總是由同一個執行緒多次獲得,故為了讓執行緒獲得鎖的代價更低而引入了偏向鎖。
當一個執行緒訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄中存儲鎖偏向的執行緒ID,以後該執行緒在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲著指向當前執行緒的偏向鎖。
- 若測試成功,表示執行緒已經獲得了鎖。
- 若測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設置成1: 若沒有設置,則使用CAS競爭鎖;如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前執行緒。
偏向鎖的撤銷
偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有正在執行的位元組碼)。
- 首先暫停擁有偏向鎖的執行緒,然後檢查持有偏向鎖的執行緒是否活著。
- 如果執行緒不處於活動狀態,則將對象頭設置成無鎖狀態;
- 如果執行緒仍然活著,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄。
- 棧中的鎖記錄和對象頭的Mark Word 要麼重新偏向其他執行緒,要麼恢復到無鎖或標記對象不適合作為偏向鎖,最後喚醒暫停的執行緒。
關閉偏向鎖
偏向鎖在Java 6 和 Java 7里是默認啟用的。但它在應用程式啟動幾秒後才激活。可以通過JVM參數來關閉延遲:
-XX:BiasedLockingStartupDelay=0
如果你確定應用程式里所有的鎖通常情況下處於競爭狀態,可以通過JVM參數關閉偏向鎖:
-XX:-UseBiasedLocking=false
設置後,程式默認會進入輕量級鎖狀態。
輕量級鎖
執行緒在執行同步塊之前,JVM會先在當前執行緒的棧幀中創建用於存儲鎖記錄的空間,並將對象中的Mark Word複製到鎖記錄中,官方稱為 Displaced Mark Word。
然後執行緒嘗試使用CAS將對象頭中的Mark Word替換位指向鎖記錄的指針:
- 如果成功,當前執行緒獲得鎖;
- 如果失敗,表示其他執行緒競爭鎖,當前執行緒便嘗試使用自旋來獲取鎖。
輕量級鎖解鎖時,會使用原子的CAS操作將Displaced Mark Word 替換會到對象頭:
- 若成功,表示沒有競爭發生。
- 若失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。
因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的執行緒被阻塞了),一旦所升級成重量級鎖,就不會再回到輕量級鎖狀態 。當鎖處於這個狀態下,其他執行緒試圖獲取鎖時,都會被阻塞,當持有鎖的執行緒被釋放鎖之後會喚醒這些執行緒,被喚醒的執行緒就會進行新一輪的奪鎖之爭。
重量級鎖
重量鎖在JVM中又叫對象監視器(Monitor),它很像C中的Mutex,除了具備Mutex互斥的功能,它還負責實現了Semaphore的功能,也就是說它至少包含一個競爭鎖的隊列,和一個訊號阻塞隊列(wait隊列),前者負責做互斥,後一個用於做執行緒同步。