Java中Synchronized的優化原理

  • 2019 年 10 月 3 日
  • 筆記

我們知道,從 JDK1.6 開始,Java 對 Synchronized 同步鎖做了充分的優化,甚至在某些場景下,它的性能已經超越了 Lock 同步鎖。那麼就讓我們來看看,它究竟是如何優化的。

原本的問題

Synchronized是基於底層作業系統的 Mutex Lock 實現的,每次獲取鎖和釋放鎖的操作都會帶來用戶態內核態的切換,從而增加系統性能開銷。

因此,在鎖競爭激烈的情況下,Synchronized同步鎖在性能上就表現得非常糟糕,它也常被大家稱為重量級鎖

到了 JDK1.5 版本,並發包中新增了 Lock 介面來實現鎖功能,它提供了與 Synchronized 關鍵字類似的同步功能,只是在使用時需要顯示獲取鎖和釋放鎖。

在單個執行緒重複申請鎖的情況下,JDK1.5 版本的 Lock 性能要比 Synchronized 鎖的性能好很多,也就是當時的 Synchronized 並不具備可重入鎖的功能。

那麼當時的 Synchronized 是怎麼實現的?又為什麼不具備可重入的功能呢?

Synchronized原理

JVM 中的同步是基於進入和退出管程(Monitor)對象實現的。每個對象實例都會有一個 Monitor,Monitor 可以和對象一起創建、銷毀。

當多個執行緒同時訪問一段同步程式碼時,多個執行緒會先被存放在EntryList集合(也可稱為阻塞隊列)中,處於BLOCKED狀態的執行緒,都會被加入到該列表。

接下來當執行緒獲取到對象的 Monitor 時,Monitor 是依靠底層作業系統的 Mutex Lock 來實現互斥的,執行緒申請 Mutex 成功,則持有該 Mutex,其它執行緒將無法獲取到該 Mutex。

如果執行緒調用 wait() 方法,就會釋放當前持有的 Mutex,並且該執行緒會進入WaitSet集合(也可稱為等待隊列)中,等待下一次被喚醒。此時執行緒會處於WAITING或者TIMEDWAITING狀態,

如果當前執行緒順利執行完方法,也將釋放 Mutex。

總的來說,就是同步鎖在這種實現方式中,因 Monitor 是依賴於底層的作業系統實現,存在用戶態內核態之間的切換(可以理解為上下文切換),所以增加了性能開銷。

鎖升級

為了提升性能,JDK1.6 引入了偏向鎖、輕量級鎖、重量級鎖概念,來減少鎖競爭帶來的上下文切換,而正是新增的Java對象頭實現了鎖升級功能。

所謂鎖升級,就是指

Synchronized 同步鎖初始為偏向鎖,隨著執行緒競爭越來越激烈,偏向鎖升級到輕量級鎖,最終升級到重量級鎖

偏向鎖

偏向鎖主要用來優化同一執行緒多次申請同一個鎖的競爭,也就是現在的Synchronized鎖實際已經擁有了可重入鎖的功能。

為什麼要有偏向鎖?因為在我們的應用中,可能大部分時間是同一個執行緒競爭鎖資源(比如單執行緒操作一個執行緒安全的容器),如果這個執行緒每次都要獲取鎖和釋放鎖,那麼就在不斷的從內核態用戶態之間切換。

那麼有了偏向鎖,當一個執行緒再次訪問這個同步程式碼或方法時,該執行緒只需去對象頭中去判斷一下是否當前執行緒是否持有該偏向鎖就可以了。

一旦出現其它執行緒競爭鎖資源時,偏向鎖就會被撤銷。偏向鎖的撤銷需要等待全局安全點(JVM的stop the world),暫停持有該鎖的執行緒,同時檢查該執行緒是否還在執行該方法,如果是,則升級鎖,反之則被其它執行緒搶佔。

輕量級鎖

當有另外一個執行緒競爭獲取這個鎖時,由於該鎖已經是偏向鎖,當發現對象頭中的執行緒 ID 不是自己的執行緒 ID,就會進行 CAS 操作獲取鎖,如果獲取成功,直接替換對象頭中的執行緒 ID 為自己的 ID,該鎖會保持偏向鎖狀態;如果獲取鎖失敗,代表當前鎖有一定的競爭,偏向鎖將升級為輕量級鎖

輕量級鎖適用於執行緒交替執行同步塊的場景,絕大部分的鎖在整個同步周期內都不存在長時間的競爭。

輕量級鎖也支援自旋,因此其他執行緒再次爭搶時,如果CAS失敗,將不再會進入阻塞狀態,而是不斷自旋。

之所以自旋更好,是因為之前說了,默認執行緒持有鎖的時間都不會太長,如果執行緒被掛起阻塞可能代價會更高。

如果自旋鎖重試之後搶鎖依然失敗,那麼同步鎖就會升級至重量級鎖

重量級鎖

在這個狀態下,未搶到鎖的執行緒都會進入 Monitor,之後會被阻塞在WaitSet集合中,也就變成了優化之前的Synchronized鎖

JVM參數優化

偏向鎖升級為輕量級鎖時,會發生stop the world,如果系統常常是多執行緒競爭,那麼禁止偏向鎖也許是更好的選擇,可以通過以下JVM參數進行優化:

// 關閉偏向鎖(默認打開)  -XX:-UseBiasedLocking  // 設置重量級鎖  -XX:+UseHeavyMonitors

輕量級鎖擁有自旋鎖的功能,那麼如果執行緒持有鎖的時間很長,那麼競爭的執行緒也會常常處於自旋狀態,佔用系統 CPU ,增加系統開銷,那麼此時關閉自旋鎖的優化可以更好一些:

-XX:-UseSpinning

總結

以上便是 Java 中針對 Synchronized 鎖的優化,也正是因為這個優化,ConcurrentHashMap 在 JDK1.8 之後,再次採用 Synchronized 鎖。如果你有什麼想法,歡迎在下方留言。

有興趣的話可以訪問我的部落格或者關注我的公眾號、頭條號,說不定會有意外的驚喜。

https://death00.github.io/