Java并發(fā)編程:Synchronized 的優(yōu)化機(jī)制
在上一篇文章中,我們學(xué)習(xí)了 Synchronized 的底層實(shí)現(xiàn)原理、JDK1.6 在偏向鎖、輕量級鎖上對 Synchronized 的優(yōu)化,以及從偏向鎖,到輕量級鎖,再到重量級鎖的升級過程。今天我們繼續(xù)看看 JDK1.6 對 Synchronized 的還做了哪些其他優(yōu)化,以及 Synchronized 的作用。
我們知道,Synchronized 鎖在升級操作后,有很大可能會變成重量級鎖,這種情況開銷會很大。開銷大的原因是因?yàn)?Synchronized 是基于底層操作系統(tǒng)的 Mutex Lock 實(shí)現(xiàn)的,每次獲取和釋放鎖操作都會帶來用戶態(tài)和內(nèi)核態(tài)的切換,從而增加系統(tǒng)性能開銷。下面我們介紹一下用戶態(tài)和內(nèi)核態(tài)。
用戶態(tài)和內(nèi)核態(tài)
用戶態(tài)(User Mode)指的是當(dāng)進(jìn)程在執(zhí)行用戶自己的代碼時,則稱其處于用戶運(yùn)行態(tài)。而當(dāng)一個任務(wù)(進(jìn)程)執(zhí)行系統(tǒng)調(diào)用而陷入內(nèi)核代碼中執(zhí)行時,我們就稱進(jìn)程處于內(nèi)核運(yùn)行態(tài),此時處理器處于特權(quán)級最高的內(nèi)核代碼中執(zhí)行。
圖片
區(qū)分了用戶態(tài)和內(nèi)核態(tài)后,程序在執(zhí)行某個操作時會進(jìn)行一系列的驗(yàn)證和檢驗(yàn),確認(rèn)沒問題之后才可以正常的操作資源,這樣就不會擔(dān)心一不小心就把系統(tǒng)搞崩潰的問題了,可以讓程序更加安全地運(yùn)行,但用戶態(tài)和內(nèi)核態(tài)這兩種形態(tài)的切換,也會導(dǎo)致一定的性能開銷。
自適應(yīng)自旋鎖
在有些場景下,對象鎖的鎖狀態(tài)只會持續(xù)很短一段時間,短時間內(nèi)頻繁地阻塞和喚醒線程是非常不值得的。 正是為了解決這一問題,我們引入了自旋鎖。
簡單來說,自旋鎖就是指通過自身循環(huán),嘗試獲取鎖的一種方式。自旋鎖在 JDK 1.4.2 中引入時,默認(rèn)為關(guān)閉,需要通過參數(shù) -XX:+UseSpinning 開啟。而到之后的 JDK1.6 中,自旋鎖則默認(rèn)為開啟,同時自旋的默認(rèn)次數(shù)為 10 次。我們可以通過參數(shù)-XX:PreBlockSpin 來調(diào)整自旋的默認(rèn)次數(shù)。但是,如果通過參數(shù)-XX:preBlockSpin 來調(diào)整自旋鎖的自旋次數(shù),會帶來諸多不便。
我們舉一個實(shí)際的例子來說明:我將參數(shù)-XX:preBlockSpin 調(diào)整為 10,自旋 10 次后若還沒有獲取鎖就退出。但在你剛剛退出的時候,可能有的線程就釋放了鎖,也就是說,在這種情況下,其實(shí)你多自旋一兩次其實(shí)就可以獲取鎖。所以我們能看到, 即使可以調(diào)整參數(shù),也還是不能徹底解決問題,于是 JDK1.6 引入自適應(yīng)的自旋鎖。
所謂自適應(yīng)就意味著自旋的次數(shù)不再是固定的,而是由前一次在同一個鎖上的自旋時間,以及鎖的擁有者狀態(tài)來決定的。
線程如果自旋成功了,那么下次自旋的次數(shù)會更加多。這是由于虛擬機(jī)會認(rèn)為既然上次成功了,那么此次自旋也很有可能會再次成功,所以它就會允許自旋等待持續(xù)的次數(shù)更多。反之,如果對于某個鎖,很少有自旋能夠成功的,那么以后在獲取這個鎖的時候,自旋的次數(shù)會減少甚至省略掉自旋過程,以免浪費(fèi)處理器資源。
下面這張圖展示了一個線程 T1 嘗試獲取鎖的操作,簡單來說:如果獲取鎖失敗,則將保持自旋;如果獲取鎖成功,就將跳出循環(huán),開始業(yè)務(wù)邏輯。
圖片
有了自適應(yīng)自旋鎖,在程序運(yùn)行和性能監(jiān)控信息的不斷完善的情況下,虛擬機(jī)對程序鎖的狀況預(yù)測會越來越準(zhǔn)確,虛擬機(jī)會變得越來越聰明。對于 Synchronized 關(guān)鍵字來說,它的自旋鎖更加地“智能”,并且 Synchronized 中的自旋鎖也是自適應(yīng)自旋鎖。
自旋鎖的優(yōu)點(diǎn)在于它避免了一些線程的掛起和恢復(fù)操作,因?yàn)閽炱鹁€程和恢復(fù)線程都需要從用戶態(tài)轉(zhuǎn)入內(nèi)核態(tài),這個過程是比較慢的,所以通過自旋的方式可以在一定程度上避免線程掛起和恢復(fù)時,所造成的性能開銷。但是,如果長時間自旋還獲取不到鎖,那么也會造成一定的資源浪費(fèi),所以我們通常會給自旋設(shè)置一個固定的值來避免一直自旋帶來的性能開銷。
鎖的優(yōu)化機(jī)制
接下來,我們一起來看一下鎖的優(yōu)化機(jī)制,其中會著重介紹到鎖膨脹、鎖消除以及鎖粗化的原理和實(shí)際操作。從中你能看到 Synchronized 在幾種鎖之間的狀態(tài)變化、用來加速程序運(yùn)行的鎖消除操作,以及提升程序執(zhí)行效率的鎖粗化操作。
鎖膨脹
鎖膨脹是指 Synchronized 從無鎖升級到偏向鎖,再到輕量級鎖,最后到重量級鎖的過程,也就是我們上一節(jié)提到的鎖升級。
圖片
在 JDK 1.6 之前,Synchronized 是重量級鎖,在釋放和獲取鎖時會從用戶態(tài)轉(zhuǎn)換成內(nèi)核態(tài),這個時候轉(zhuǎn)換的效率是比較低的。JDK 從 1.6 開始,就引入了鎖膨脹機(jī)制,Synchronized 的狀態(tài)就多了無鎖、偏向鎖以及輕量級鎖。這個時候在進(jìn)行并發(fā)操作時,大部分的場景就不需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài)了,也因此 Synchronized 的性能得到了大幅度的提升。
鎖消除
鎖消除是指在某些情況下,JVM 虛擬機(jī)如果檢測不到某段代碼被共享或競爭的可能性,那么就會將這段代碼所屬的同步鎖消除掉,從而達(dá)到提高程序性能的目的。
鎖消除的依據(jù)是逃逸分析的數(shù)據(jù)支持,就像 StringBuffer 的 append() 方法,或 Vector 的 add() 方法,在很多情況下都是可以進(jìn)行鎖消除的。我們來看下面這個代碼示例:
public String method() {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 10; i++) {
sb.append("i:" + i);
}
return sb.toString();
}
以上代碼經(jīng)過編譯之后的字節(jié)碼如下:
圖片
從上述字節(jié)碼結(jié)果可以看出,之前我們寫的線程安全的、加鎖的 StringBuffer 對象,在生成字節(jié)碼之后就被替換成了線程不安全的、不加鎖的 StringBuilder 對象了。原因是 StringBuffer 的變量屬于一個局部變量,不會從該方法中逃逸出去,此時我們就可以使用鎖消除來加速程序的運(yùn)行。
鎖粗化
鎖粗化指的是,將多個連續(xù)的加鎖、解鎖操作連接在一起,擴(kuò)展成一個范圍更大的鎖。我們知道縮小鎖的范圍后,在鎖競爭時,等待獲取鎖的線程可以更早地獲取鎖,提高程序的運(yùn)行效率,這系列的操作,我們稱之為鎖“細(xì)化”。
鎖細(xì)化的觀點(diǎn)在大多數(shù)情況下都是成立的,但是一系列連續(xù)加鎖和解鎖的操作,也會導(dǎo)致不必要的性能開銷,從而影響程序的執(zhí)行效率,所以我們才需要進(jìn)行鎖粗化的操作。那么鎖粗化是如何提高性能的呢?我們來看下面的例子:
public String test() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
// 加鎖操作
// ...
sb.append("i:" + i);
// 偽代碼:解鎖操作
// ...
}
return sb.toString();
}
這里我們不考慮編譯器優(yōu)化的情況。我們可以看到每次 for 循環(huán)都需要進(jìn)行加鎖和釋放鎖的操作,性能是很低的;但如果我們直接在 for 循環(huán)的外層加一把鎖,那么對于同一個對象操作這段代碼的性能就會提高很多,如下代碼所示:
public String test() {
StringBuilder sb = new StringBuilder();
// 加鎖操作
// ...
for (int i = 0; i < 10; i++) {
sb.append("i:" + i);
}
// 解鎖操作
// ...
return sb.toString();
}
鎖粗化的作用:如果檢測到同一個對象執(zhí)行了連續(xù)的加鎖和解鎖的操作,則會將這一系列操作合并成一個更大的鎖,從而提升程序的執(zhí)行效率。其表現(xiàn)為,分散在不同地方的 Synchronized 語句塊會根據(jù)代碼邏輯自動合并。JVM 會根據(jù) Synchronized 加鎖解鎖的總時間開銷來自行決定合并 Synchronized 語句塊的時機(jī)。
總結(jié)
今天我們首先介紹了一下用戶態(tài)和內(nèi)核態(tài)的內(nèi)容。通常我們的應(yīng)用代碼都是用戶態(tài)的,會和系統(tǒng)的內(nèi)核交互,這樣做的好處是保證系統(tǒng)權(quán)限的安全。之后,我們介紹了 JVM 底層對鎖的幾種優(yōu)化方式,其中自旋可能是大家最為熟悉的操作。但要注意的是,在很多 CAS 的場景中需要我們手動地通過編碼的方式完成自旋操作,而本文提到的自旋是 JVM 底層幫我們做的事情,這是大家要注意區(qū)分的地方。
從 JDK1.6 開始,JVM 幫我們實(shí)現(xiàn)的自旋,變得很智能,可以自動選擇自旋的次數(shù)。此外 JVM 可以根據(jù)代碼的實(shí)際運(yùn)行情況自動地對鎖進(jìn)行升級,這是另一個體現(xiàn) JVM 對鎖實(shí)現(xiàn)智能控制的地方。鎖膨脹是 Synchronized 的核心,Synchronized 通過偏向鎖轉(zhuǎn)化到輕量級鎖再轉(zhuǎn)化到重量級鎖來完成膨脹過程;而鎖的消除和粗化更像是一對相關(guān)的操作,消除是讓不必要的 Synchronized 語句塊消失,而粗化是讓多個小的 Synchronized 合并成一個大的 Synchronized 語句塊。