Java 并發(fā)編程學(xué)習(xí)筆記之Synchronized底層優(yōu)化
一、重量級(jí)鎖
上篇文章中向大家介紹了Synchronized的用法及其實(shí)現(xiàn)的原理。現(xiàn)在我們應(yīng)該知道,Synchronized是通過對(duì)象內(nèi)部的一個(gè)叫做監(jiān)視器鎖(monitor)來實(shí)現(xiàn)的。但是監(jiān)視器鎖本質(zhì)又是依賴于底層的操作系統(tǒng)的Mutex Lock來實(shí)現(xiàn)的。而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換這就需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)成本非常高,狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間,這就是為什么Synchronized效率低的原因。因此,這種依賴于操作系統(tǒng)Mutex Lock所實(shí)現(xiàn)的鎖我們稱之為“重量級(jí)鎖”。JDK中對(duì)Synchronized做的種種優(yōu)化,其核心都是為了減少這種重量級(jí)鎖的使用。JDK1.6以后,為了減少獲得鎖和釋放鎖所帶來的性能消耗,提高性能,引入了“輕量級(jí)鎖”和“偏向鎖”。
二、輕量級(jí)鎖
鎖的狀態(tài)總共有四種:無鎖狀態(tài)、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖。隨著鎖的競(jìng)爭(zhēng),鎖可以從偏向鎖升級(jí)到輕量級(jí)鎖,再升級(jí)的重量級(jí)鎖(但是鎖的升級(jí)是單向的,也就是說只能從低到高升級(jí),不會(huì)出現(xiàn)鎖的降級(jí))。JDK 1.6中默認(rèn)是開啟偏向鎖和輕量級(jí)鎖的,我們也可以通過-XX:-UseBiasedLocking來禁用偏向鎖。鎖的狀態(tài)保存在對(duì)象的頭文件中,以32位的JDK為例:
鎖狀態(tài) |
25 bit |
4bit |
1bit |
2bit |
||
23bit |
2bit |
是否是偏向鎖 |
鎖標(biāo)志位 |
|||
輕量級(jí)鎖 |
指向棧中鎖記錄的指針 |
00 |
||||
重量級(jí)鎖 |
指向互斥量(重量級(jí)鎖)的指針 |
10 |
||||
GC標(biāo)記 |
空 |
11 |
||||
偏向鎖 |
線程ID |
Epoch |
對(duì)象分代年齡 |
1 |
01 |
|
無鎖 |
對(duì)象的hashCode |
對(duì)象分代年齡 |
0 |
01 |
“輕量級(jí)”是相對(duì)于使用操作系統(tǒng)互斥量來實(shí)現(xiàn)的傳統(tǒng)鎖而言的。但是,首先需要強(qiáng)調(diào)一點(diǎn)的是,輕量級(jí)鎖并不是用來代替重量級(jí)鎖的,它的本意是在沒有多線程競(jìng)爭(zhēng)的前提下,減少傳統(tǒng)的重量級(jí)鎖使用產(chǎn)生的性能消耗。在解釋輕量級(jí)鎖的執(zhí)行過程之前,先明白一點(diǎn),輕量級(jí)鎖所適應(yīng)的場(chǎng)景是線程交替執(zhí)行同步塊的情況,如果存在同一時(shí)間訪問同一鎖的情況,就會(huì)導(dǎo)致輕量級(jí)鎖膨脹為重量級(jí)鎖。
1、輕量級(jí)鎖的加鎖過程
?。?)在代碼進(jìn)入同步塊的時(shí)候,如果同步對(duì)象鎖狀態(tài)為無鎖狀態(tài)(鎖標(biāo)志位為“01”狀態(tài),是否為偏向鎖為“0”),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word。這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如圖2.1所示。
?。?)拷貝對(duì)象頭中的Mark Word復(fù)制到鎖記錄中。
?。?)拷貝成功后,虛擬機(jī)將使用CAS操作嘗試將對(duì)象的Mark Word更新為指向Lock Record的指針,并將Lock record里的owner指針指向object mark word。如果更新成功,則執(zhí)行步驟(3),否則執(zhí)行步驟(4)。
?。?)如果這個(gè)更新動(dòng)作成功了,那么這個(gè)線程就擁有了該對(duì)象的鎖,并且對(duì)象Mark Word的鎖標(biāo)志位設(shè)置為“00”,即表示此對(duì)象處于輕量級(jí)鎖定狀態(tài),這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如圖2.2所示。
?。?)如果這個(gè)更新操作失敗了,虛擬機(jī)首先會(huì)檢查對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀,如果是就說明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行。否則說明多個(gè)線程競(jìng)爭(zhēng)鎖,輕量級(jí)鎖就要膨脹為重量級(jí)鎖,鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”,Mark Word中存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)。 而當(dāng)前線程便嘗試使用自旋來獲取鎖,自旋就是為了不讓線程阻塞,而采用循環(huán)去獲取鎖的過程。
圖2.1 輕量級(jí)鎖CAS操作之前堆棧與對(duì)象的狀態(tài)
圖2.2 輕量級(jí)鎖CAS操作之后堆棧與對(duì)象的狀態(tài)
2、輕量級(jí)鎖的解鎖過程:
(1)通過CAS操作嘗試把線程中復(fù)制的Displaced Mark Word對(duì)象替換當(dāng)前的Mark Word。
(2)如果替換成功,整個(gè)同步過程就完成了。
?。?)如果替換失敗,說明有其他線程嘗試過獲取該鎖(此時(shí)鎖已膨脹),那就要在釋放鎖的同時(shí),喚醒被掛起的線程。
三、偏向鎖
引入偏向鎖是為了在無多線程競(jìng)爭(zhēng)的情況下盡量減少不必要的輕量級(jí)鎖執(zhí)行路徑,因?yàn)檩p量級(jí)鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時(shí)候依賴一次CAS原子指令(由于一旦出現(xiàn)多線程競(jìng)爭(zhēng)的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的性能損耗必須小于節(jié)省下來的CAS原子指令的性能消耗)。上面說過,輕量級(jí)鎖是為了在線程交替執(zhí)行同步塊時(shí)提高性能,而偏向鎖則是在只有一個(gè)線程執(zhí)行同步塊時(shí)進(jìn)一步提高性能。
1、偏向鎖獲取過程:
?。?)訪問Mark Word中偏向鎖的標(biāo)識(shí)是否設(shè)置成1,鎖標(biāo)志位是否為01——確認(rèn)為可偏向狀態(tài)。
(2)如果為可偏向狀態(tài),則測(cè)試線程ID是否指向當(dāng)前線程,如果是,進(jìn)入步驟(5),否則進(jìn)入步驟(3)。
?。?)如果線程ID并未指向當(dāng)前線程,則通過CAS操作競(jìng)爭(zhēng)鎖。如果競(jìng)爭(zhēng)成功,則將Mark Word中線程ID設(shè)置為當(dāng)前線程ID,然后執(zhí)行(5);如果競(jìng)爭(zhēng)失敗,執(zhí)行(4)。
(4)如果CAS獲取偏向鎖失敗,則表示有競(jìng)爭(zhēng)。當(dāng)?shù)竭_(dá)全局安全點(diǎn)(safepoint)時(shí)獲得偏向鎖的線程被掛起,偏向鎖升級(jí)為輕量級(jí)鎖,然后被阻塞在安全點(diǎn)的線程繼續(xù)往下執(zhí)行同步代碼。
?。?)執(zhí)行同步代碼。
2、偏向鎖的釋放:
偏向鎖的撤銷在上述第四步驟中有提到。偏向鎖只有遇到其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖,線程不會(huì)主動(dòng)去釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒有字節(jié)碼正在執(zhí)行),它會(huì)首先暫停擁有偏向鎖的線程,判斷鎖對(duì)象是否處于被鎖定狀態(tài),撤銷偏向鎖后恢復(fù)到未鎖定(標(biāo)志位為“01”)或輕量級(jí)鎖(標(biāo)志位為“00”)的狀態(tài)。
3、重量級(jí)鎖、輕量級(jí)鎖和偏向鎖之間轉(zhuǎn)換
圖 2.3三者的轉(zhuǎn)換圖
該圖主要是對(duì)上述內(nèi)容的總結(jié),如果對(duì)上述內(nèi)容有較好的了解的話,該圖應(yīng)該很容易看懂。
四、其他優(yōu)化
1、適應(yīng)性自旋(Adaptive Spinning):從輕量級(jí)鎖獲取的流程中我們知道,當(dāng)線程在獲取輕量級(jí)鎖的過程中執(zhí)行CAS操作失敗時(shí),是要通過自旋來獲取重量級(jí)鎖的。問題在于,自旋是需要消耗CPU的,如果一直獲取不到鎖的話,那該線程就一直處在自旋狀態(tài),白白浪費(fèi)CPU資源。解決這個(gè)問題最簡(jiǎn)單的辦法就是指定自旋的次數(shù),例如讓其循環(huán)10次,如果還沒獲取到鎖就進(jìn)入阻塞狀態(tài)。但是JDK采用了更聰明的方式——適應(yīng)性自旋,簡(jiǎn)單來說就是線程如果自旋成功了,則下次自旋的次數(shù)會(huì)更多,如果自旋失敗了,則自旋的次數(shù)就會(huì)減少。
2、鎖粗化(Lock Coarsening):鎖粗化的概念應(yīng)該比較好理解,就是將多次連接在一起的加鎖、解鎖操作合并為一次,將多個(gè)連續(xù)的鎖擴(kuò)展成一個(gè)范圍更大的鎖。舉個(gè)例子:
package com.paddx.test.string; public class StringBufferTest { StringBuffer stringBuffer = new StringBuffer(); public void append(){ stringBuffer.append("a"); stringBuffer.append("b"); stringBuffer.append("c"); } }
這里每次調(diào)用stringBuffer.append方法都需要加鎖和解鎖,如果虛擬機(jī)檢測(cè)到有一系列連串的對(duì)同一個(gè)對(duì)象加鎖和解鎖操作,就會(huì)將其合并成一次范圍更大的加鎖和解鎖操作,即在第一次append方法時(shí)進(jìn)行加鎖,最后一次append方法結(jié)束后進(jìn)行解鎖。
3、鎖消除(Lock Elimination):鎖消除即刪除不必要的加鎖操作。根據(jù)代碼逃逸技術(shù),如果判斷到一段代碼中,堆上的數(shù)據(jù)不會(huì)逃逸出當(dāng)前線程,那么可以認(rèn)為這段代碼是線程安全的,不必要加鎖??聪旅孢@段程序:
package com.paddx.test.concurrent; public class SynchronizedTest02 { public static void main(String[] args) { SynchronizedTest02 test02 = new SynchronizedTest02(); //啟動(dòng)預(yù)熱 for (int i = 0; i < 10000; i++) { i++; } long start = System.currentTimeMillis(); for (int i = 0; i < 100000000; i++) { test02.append("abc", "def"); } System.out.println("Time=" + (System.currentTimeMillis() - start)); } public void append(String str1, String str2) { StringBuffer sb = new StringBuffer(); sb.append(str1).append(str2); } }
雖然StringBuffer的append是一個(gè)同步方法,但是這段程序中的StringBuffer屬于一個(gè)局部變量,并且不會(huì)從該方法中逃逸出去,所以其實(shí)這過程是線程安全的,可以將鎖消除。下面是我本地執(zhí)行的結(jié)果:
為了盡量減少其他因素的影響,這里禁用了偏向鎖(-XX:-UseBiasedLocking)。通過上面程序,可以看出消除鎖以后性能還是有比較大提升的。
注:可能JDK各個(gè)版本之間執(zhí)行的結(jié)果不盡相同,我這里采用的JDK版本為1.6。
五、總結(jié)
本文重點(diǎn)介紹了JDk中采用輕量級(jí)鎖和偏向鎖等對(duì)Synchronized的優(yōu)化,但是這兩種鎖也不是完全沒缺點(diǎn)的,比如競(jìng)爭(zhēng)比較激烈的時(shí)候,不但無法提升效率,反而會(huì)降低效率,因?yàn)槎嗔艘粋€(gè)鎖升級(jí)的過程,這個(gè)時(shí)候就需要通過-XX:-UseBiasedLocking來禁用偏向鎖。下面是這幾種鎖的對(duì)比:
鎖 |
優(yōu)點(diǎn) |
缺點(diǎn) |
適用場(chǎng)景 |
偏向鎖 |
加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法比僅存在納秒級(jí)的差距。 |
如果線程間存在鎖競(jìng)爭(zhēng),會(huì)帶來額外的鎖撤銷的消耗。 |
適用于只有一個(gè)線程訪問同步塊場(chǎng)景。 |
輕量級(jí)鎖 |
競(jìng)爭(zhēng)的線程不會(huì)阻塞,提高了程序的響應(yīng)速度。 |
如果始終得不到鎖競(jìng)爭(zhēng)的線程使用自旋會(huì)消耗CPU。 |
追求響應(yīng)時(shí)間。 同步塊執(zhí)行速度非???。 |
重量級(jí)鎖 |
線程競(jìng)爭(zhēng)不使用自旋,不會(huì)消耗CPU。 |
線程阻塞,響應(yīng)時(shí)間緩慢。 |
追求吞吐量。 同步塊執(zhí)行速度較長(zhǎng)。 |
相關(guān)文章
Java數(shù)據(jù)結(jié)構(gòu)之雙端鏈表原理與實(shí)現(xiàn)方法
這篇文章主要介紹了Java數(shù)據(jù)結(jié)構(gòu)之雙端鏈表原理與實(shí)現(xiàn)方法,簡(jiǎn)單描述了雙端鏈表的概念、原理并結(jié)合實(shí)例形式分析了java實(shí)現(xiàn)雙端鏈表的相關(guān)操作技巧,需要的朋友可以參考下2017-10-10java數(shù)據(jù)結(jié)構(gòu)圖論霍夫曼樹及其編碼示例詳解
這篇文章主要為大家介紹了java數(shù)據(jù)結(jié)構(gòu)圖論霍夫曼樹及其編碼示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2021-11-11springboot接口多實(shí)現(xiàn)類選擇性注入解決方案
這篇文章主要為大家介紹了springboot接口多實(shí)現(xiàn)類選擇性注入解決方案的四種方式,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-03-03SpringBoot+Tess4j實(shí)現(xiàn)牛逼的OCR識(shí)別工具的示例代碼
這篇文章主要介紹了SpringBoot+Tess4j實(shí)現(xiàn)牛逼的OCR識(shí)別工具的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01Java ==,equals()與hashcode()的使用
本文主要介紹了Java ==,equals()與hashcode()的使用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05JAVA基礎(chǔ)類庫之String類,StringBuffer類和StringBuilder類
這篇文章主要介紹了Java中基礎(chǔ)類庫的String類,StringBuffer類和StringBuilder類,是Java入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2021-09-09Java設(shè)置PDF有序和無序列表的知識(shí)點(diǎn)總結(jié)
在本篇文章中小編給大家整理了關(guān)于Java設(shè)置PDF有序和無序列表的知識(shí)點(diǎn),需要的朋友們參考下。2019-03-03