老掉牙的 synchronized 鎖優化,一次給你講清楚!
- 2022 年 7 月 4 日
- 筆記

我們都知道 synchronized 關鍵字能實現執行緒安全,但是你知道這背後的原理是什麼嗎?今天我們就來講一講 synchronized 實現執行緒同步背後的原因,以及相關的鎖優化策略吧。
synchronized 背後的原理
synchronized 關鍵字經過編譯之後,會在同步塊的前後分別形成 monitorenter 和 monitorexit 這兩個位元組碼指令,這兩個位元組碼只需要一個指明一個要鎖定或解鎖的對象。如果 Java 程式中指明了對象參數,那麼就用這個對象作為鎖。
如果沒有指定,那麼就根據 synchronized 修飾的是實例方法還是類方法,去拿對應的對象實例或 Class 對象來作為鎖對象。因此我們可以知道,synchronized 關鍵字實現執行緒同步的背後,其實是 Java 虛擬機規範對於 monitorenter 和 monitorexit 的定義。
在 Java 虛擬機規範對 monitorenter 和 monitorexit 的行為描述中,有兩點需要特別注意。
- synchronized 同步塊對同一條執行緒是可重入的,也就是不會出現自己把自己鎖死的問題。
- 同步塊在已進入的執行緒執行完之前,會阻塞後面其他執行緒的進入。
synchronized 關鍵字在 JDK1.6 版本之前,是通過作業系統的 Mutex Lock 來實現同步的。而作業系統的 Mutex Lock 是作業系統級別的方法,需要切換到內核態來執行。這就需要從用戶態轉換到內核態中,因此我們說 synchronized 同步是重量級的操作。
synchronized 鎖優化
在 JDK1.6 版本中,HotSpot 虛擬機開發團隊花了很大的精力去實現各種鎖優化技術,如:適應性自旋、鎖消除、鎖粗話、偏向鎖、輕量級鎖等。其中最重要的是:自旋鎖、輕量級鎖、偏向鎖這三個,我們重點講這三個鎖優化。
自旋鎖與自適應自旋
對於重量級的同步操作來說,最大的消耗其實是內核態與用戶態的切換。但很多時候,對於共享數據的操作時間可能很短,比內核態切換到用戶態這個耗時還短。
於是有人就想:如果有多個執行緒並發去獲取鎖的時候,如果能讓後面那個請求鎖的執行緒「稍等一下」,不放棄 CPU 的執行時間,看看持有鎖的執行緒是否會很快釋放鎖。為了讓執行緒等待,我們只需讓執行緒執行一個忙循環(自旋),這項技術就是所謂的自旋鎖。 從理論上來看,如果所有執行緒都很快地獲取鎖、釋放鎖,那麼自旋鎖是可以帶來較大的性能提升的。自旋鎖在 JDK 1.4.2 中就已經引入,默認自旋 10 次。但自旋鎖默認是關閉的,在 JDK 1.6 中才改為默認開啟了。
自旋等待雖然避免了執行緒切換的開銷,但還是要佔用處理器的時間。如果鎖被佔用的時間段,自旋等待的效果就會非常好。但如果鎖被長時間佔用,那麼自旋的執行緒就會白白消耗處理器的資源,從而帶來性能上的浪費。
為了解決特殊情況下自旋鎖的性能消耗問題,在 JDK1.6 的時候引入了自適應的自旋鎖。 自適應意味著自旋時間不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者狀態決定。如果在同一鎖對象上,自旋等待剛剛成功獲得過鎖,那麼虛擬機認為這次自旋也很有可能再次成功,進而允許執行緒自旋更長時間,例如自旋 100 個循環。
但如果對於某個鎖,自旋很少成功獲得過。那虛擬機為了避免浪費 CPU 資源,有可能省略掉自旋過程。有了自旋鎖,隨著程式運行和性能監控資訊的不斷完善,虛擬機對鎖的狀態預測就越准,虛擬機也會變得越來越聰明。
輕量級鎖
輕量級鎖是 JDK1.6 加入的新型鎖機制,名字中的「輕量級」是相對於作業系統互斥量這個重量級鎖而言的。輕量級鎖誕生的原因,是由於對於絕大部分的鎖而言,整個同步周期都不存在競爭。如果沒有競爭的話,那就沒必要使用重量級鎖了,於是就誕生了輕量級鎖來提高效率。
對於輕量級鎖來說,其同步的流程如下:
- 在程式碼進入同步塊的時候,如果此同步對象沒有被鎖定(鎖標誌位為 01 狀態),那麼虛擬機會在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的 Mark Word 拷貝。
- 虛擬機將使用 CAS 操作嘗試將對象的 Mark Word 更新為指向 Lock Record 的指針。如果更新動作成功了,那麼執行緒就泳衣了該對象的鎖,並且對象 Mark Word 的鎖標誌位就變成了 00,表示此對象處於輕量級鎖定狀態。
簡單地說,輕量級鎖的同步流程可以總結為:使用 CAS 操作,在執行緒棧幀與鎖對象建立雙向的指針。
在沒有執行緒競爭的情況下,輕量級鎖使用 CAS 自旋操作避免了使用互斥量的開銷,提高了效率。但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了 CAS 操作。因此在有競爭的情況下,輕量級鎖會比傳統的重量級鎖更慢。
偏向鎖
偏向鎖是 JDK1.6 中引入的一項優化,它的意思是這個鎖會偏向於第一個獲得它的執行緒。如果在接下來的執行過程中,該鎖沒有被其他執行緒獲取,則持有偏向鎖的執行緒將永遠不需要再進行同步。 對於偏向鎖來說,其同步流程如下所示:
- 假設當前虛擬機啟動了偏向鎖,那麼當鎖對象第一次被執行緒獲取的時候,虛擬機將會把對象的鎖標誌位設置為 01,偏向鎖位設置為 1。同時使用 CAS 操作將執行緒 ID 記錄在對象的 MarkWord 之中。如果 CAS 操作成功,那麼持有偏向鎖的執行緒進入鎖對應的同步塊時,虛擬機將不再進行任何同步操作。
- 當有另外一個執行緒嘗試去獲取這個鎖時,根據鎖對象目前是否處於鎖定狀態,將其恢復到未鎖定(01)或輕量級鎖定(00)狀態。隨後的同步操作,就向上面介紹的輕量級鎖那樣執行。
可以看到偏向鎖還是需要做一些 CAS 操作,但是對比起輕量級鎖來說,其要設置的內容大大減少了,因此也提高了一些效率。偏向鎖可以提高帶有同步但無競爭的程式性能。 它同樣是一個帶有效益權衡(Trade Off)性質的優化,也就是說,它並不一定總是對程式運行有利,如果程式中大多數的鎖總是被多個不同的執行緒訪問,那偏向模式就是多餘的。
優化後的鎖獲取流程
經過 JDK1.6 的優化,synchronized 同步機制的流程變成了:
- 首先,synchronized 會嘗試使用偏向鎖的方式去競爭鎖資源,如果能夠競爭到偏向鎖,表示加鎖成功直接返回。
- 如果競爭鎖失敗,說明當前鎖已經偏向了其他執行緒。需要將鎖升級到輕量級鎖,在輕量級鎖狀態下,競爭鎖的執行緒根據自適應自旋次數去嘗試搶佔鎖資源。
- 如果在輕量級鎖狀態下還是沒有競爭到鎖,就只能升級到重量級鎖。在重量級鎖狀態下,沒有競爭到鎖的執行緒就會被阻塞。處於鎖等待狀態的執行緒需要等待獲得鎖的執行緒來觸發喚醒。
上面的鎖獲取流程,可以用如下的示意圖來表示:

總結
本文首先簡單講解了 synchronized 關鍵字實現同步的原理,其實是通過 Java 虛擬機規範對於 monitorenter 和 monitorexit 的支援,從而使得 synchronized 能夠實現同步。而 synchronized 同步本質上是通過作業系統的 mutex 鎖來實現的。由於操作作業系統 mutex 鎖太過於消耗資源,因此在 JDK1.6 後 HotSpot 虛擬機做了一系列的鎖優化,其中最重要的便是:自旋鎖、輕量級鎖、偏向鎖。這三個鎖的誕生原因,以及提升的點如下表所示。
| 現狀 | 鎖名稱 | 收益 | 使用場景 |
|---|---|---|---|
| 大多數情況下,等待鎖的時間比作業系統 mutex 短得多 | 自旋鎖 | 減少內核態與用戶態切換的開銷 | 執行緒獲取鎖時間較短的情況 |
| 大多數情況下,鎖同步期間沒有執行緒競爭 | 輕量級鎖 | 與自旋鎖相比,減少了自旋時間 | 沒有執行緒競爭鎖 |
| 大多數情況下,鎖同步期間沒有執行緒競爭 | 偏向鎖 | 與輕量級鎖相比,減少了多餘的對象複製操作 | 沒有執行緒競爭鎖 |
從上面表格可以看到,自旋鎖、輕量級鎖、偏向鎖,他們的優化是逐漸深入的。
- 對於重量級鎖來說,自旋鎖減少了互斥量的內核、用戶態切換開銷。
- 輕量級鎖,是自旋鎖再 Java 記憶體模型里的直接應用,其同樣是減少了內核態與用戶態的切換開銷。
- 偏向鎖,相對於輕量級鎖來說,減少了多餘的對象複製操作,因此效率更高一些。
參考資料
- 深入理解 Java 虛擬機:JVM 高級特性與最佳實踐(第 2 版)- 周志明 - 微信讀書
- 【Java 面試】為什麼引入偏向鎖、輕量級鎖,介紹下升級流程 - 跟著 Mic 學架構 - 部落格園
- 不可不說的 Java 「鎖」 事 - 美團技術團隊
