synchronized 在 JDK 1.5 時效能是比較低的,然而在後續的版本中經過各種優化迭代,它的效能也得到了前所未有的提升,上一篇中我們談到了鎖膨脹對 synchronized 效能的提升,然而它也只是“眾多” synchronized 效能優化方案中的一種,那麼我們本文就來盤點一下 synchronized 的核心優化方案。
synchronized 核心優化方案主要包含以下 4 個:
- 鎖膨脹
- 鎖消除
- 鎖粗化
- 自適應自旋鎖
1.鎖膨脹
我們先來回顧一下鎖膨脹對 synchronized 效能的影響,所謂的鎖膨脹是指 synchronized 從無鎖升級到偏向鎖,再到輕量級鎖,最後到重量級鎖的過程,它叫做鎖膨脹也叫做鎖升級。
JDK 1.6 之前,synchronized 是重量級鎖,也就是說 synchronized 在釋放和獲取鎖時都會從使用者態轉換成核心態,而轉換的效率是比較低的。但有了鎖膨脹機制之後,synchronized 的狀態就多了無鎖、偏向鎖以及輕量級鎖了,這時候在進行併發操作時,大部分的場景都不需要使用者態到核心態的轉換了,這樣就大幅的提升了 synchronized 的效能。
PS:至於為什麼不需要使用者態到核心態的轉換?請移步到鎖膨脹的那篇文章:《synchronized 優化手段之鎖膨脹機制》。
2.鎖消除
很多人都瞭解 synchronized 中鎖膨脹的機制,但對接下來的 3 項優化卻知之甚少,這樣會在面試中錯失良機,那麼我們本文就把這 3 項優化單獨拎出來講一下吧。
鎖消除指的是在某些情況下,JVM 虛擬機器如果檢測不到某段程式碼被共享和競爭的可能性,就會將這段程式碼所屬的同步鎖消除掉,從而到底提高程式效能的目的。
鎖消除的依據是逃逸分析的資料支援,如 StringBuffer 的 append() 方法,或 Vector 的 add() 方法,在很多情況下是可以進行鎖消除的,比如以下這段程式碼:
public String method() {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 10; i++) {
sb.append("i:" + i);
}
return sb.toString();
}
以上程式碼經過編譯之後的位元組碼如下:
從上述結果可以看出,之前我們寫的執行緒安全的加鎖的 StringBuffer 物件,在生成位元組碼之後就被替換成了不加鎖不安全的 StringBuilder 物件了,原因是 StringBuffer 的變數屬於一個區域性變數,並且不會從該方法中逃逸出去,所以此時我們就可以使用鎖消除(不加鎖)來加速程式的執行。
3.鎖粗化
鎖粗化是指,將多個連續的加鎖、解鎖操作連線在一起,擴充套件成一個範圍更大的鎖。
我只聽說鎖“細化”可以提高程式的執行效率,也就是將鎖的範圍儘可能縮小,這樣在鎖競爭時,等待獲取鎖的執行緒才能更早的獲取鎖,從而提高程式的執行效率,但鎖粗化是如何提高效能的呢?
沒錯,鎖細化的觀點在大多數情況下都是成立了,但是一系列連續加鎖和解鎖的操作,也會導致不必要的效能開銷,從而影響程式的執行效率,比如這段程式碼:
public String method() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
// 虛擬碼:加鎖操作
sb.append("i:" + i);
// 虛擬碼:解鎖操作
}
return sb.toString();
}
這裡我們不考慮編譯器優化的情況,如果在 for 迴圈中定義鎖,那麼鎖的範圍很小,但每次 for 迴圈都需要進行加鎖和釋放鎖的操作,效能是很低的;但如果我們直接在 for 迴圈的外層加一把鎖,那麼對於同一個物件操作這段程式碼的效能就會提高很多,如下虛擬碼所示:
public String method() {
StringBuilder sb = new StringBuilder();
// 虛擬碼:加鎖操作
for (int i = 0; i < 10; i++) {
sb.append("i:" + i);
}
// 虛擬碼:解鎖操作
return sb.toString();
}
鎖粗化的作用:如果檢測到同一個物件執行了連續的加鎖和解鎖的操作,則會將這一系列操作合併成一個更大的鎖,從而提升程式的執行效率。
4.自適應自旋鎖
自旋鎖是指通過自身迴圈,嘗試獲取鎖的一種方式,虛擬碼實現如下:
// 嘗試獲取鎖
while(!isLock()){
}
自旋鎖優點在於它避免一些執行緒的掛起和恢復操作,因為掛起執行緒和恢復執行緒都需要從使用者態轉入核心態,這個過程是比較慢的,所以通過自旋的方式可以一定程度上避免執行緒掛起和恢復所造成的效能開銷。
但是,如果長時間自旋還獲取不到鎖,那麼也會造成一定的資源浪費,所以我們通常會給自旋設定一個固定的值來避免一直自旋的效能開銷。然而對於 synchronized 關鍵字來說,它的自旋鎖更加的“智慧”,synchronized 中的自旋鎖是自適應自旋鎖,這就好比之前一直開的手動擋的三輪車,而經過了 JDK 1.6 的優化之後,我們的這部“車”,一下子變成自動擋的蘭博基尼了。
自適應自旋鎖是指,執行緒自旋的次數不再是固定的值,而是一個動態改變的值,這個值會根據前一次自旋獲取鎖的狀態來決定此次自旋的次數。比如上一次通過自旋成功獲取到了鎖,那麼這次通過自旋也有可能會獲取到鎖,所以這次自旋的次數就會增多一些,而如果上一次通過自旋沒有成功獲取到鎖,那麼這次自旋可能也獲取不到鎖,所以為了避免資源的浪費,就會少迴圈或者不迴圈,以提高程式的執行效率。簡單來說,如果執行緒自旋成功了,則下次自旋的次數會增多,如果失敗,下次自旋的次數會減少。
總結
本文我們介紹了 4 種優化 synchronized 的方案,其中鎖膨脹和自適應自旋鎖是 synchronized 關鍵字自身的優化實現,而鎖消除和鎖粗化是 JVM 虛擬機器對 synchronized 提供的優化方案,這些優化方案最終使得 synchronized 的效能得到了大幅的提升,也讓它在併發程式設計中佔據了一席之地。
參考 & 鳴謝
www.cnblogs.com/aspirant/p/11470858.html
zhuanlan.zhihu.com/p/29866981
tech.meituan.com/2018/11/15/java-lock.html
本系列推薦文章
- 併發第一課:Thread 詳解
- Java中使用者執行緒和守護執行緒區別這麼大?
- 深入理解執行緒池 ThreadPool
- 執行緒池的7種建立方式,強烈推薦你用它...
- 池化技術到達有多牛?看了執行緒和執行緒池的對比嚇我一跳!
- 併發中的執行緒同步與鎖
- synchronized 加鎖 this 和 class 的區別!
- volatile 和 synchronized 的區別
- 輕量級鎖一定比重量級鎖快嗎?
- 這樣終止執行緒,竟然會導致服務宕機?
- SimpleDateFormat執行緒不安全的5種解決方案!
- ThreadLocal不好用?那是你沒用對!
- ThreadLocal記憶體溢位程式碼演示和原因分析!
- Semaphore自白:限流器用我就對了!
- CountDownLatch:別浪,等人齊再團!
- CyclicBarrier:人齊了,司機就可以發車了!
- synchronized 優化手段之鎖膨脹機制!