互斥同步、鎖優(yōu)化及synchronized和volatile

互斥同步

互斥同步(Mutual Exclusion & Synchronization)是常見的一種并發(fā)正確性保證手段。同步是指子啊多個(gè)線程并發(fā)訪問共享數(shù)據(jù)時(shí),保證共享數(shù)據(jù)在同一時(shí)刻只能被一個(gè)(或者是一些,使用信號(hào)量的時(shí)候)線程使用。而互斥是實(shí)現(xiàn)同步的一種手段,臨界區(qū)(Critial Section)、互斥量(Mutex)和信號(hào)量(Semaphore)都是主要的互斥實(shí)現(xiàn)方式。因此,在這四個(gè)字里面,互斥是因,同步是果;互斥是方法,同步是目的。

synchronized的實(shí)現(xiàn)

在Java中,大家都知道,synchronized關(guān)鍵字是最基本的互斥同步手段??匆欢魏?jiǎn)單的代碼:

public static void main(String[] args)
{
    synchronized (TestMain.class)
    {
        
    }
}

這段代碼被編譯之后是這樣的:

public static void main(java.lang.String[]);
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=2, locals=1, args_size=1
       0: ldc           #1                  // class com/xrq/test53/TestMain
       2: dup
       3: monitorenter
       4: monitorexit
       5: return
    LineNumberTable:
      line 7: 0
      line 11: 5
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
             0       6     0  args   [Ljava/lang/String;

關(guān)鍵就在第7行和第8行,在源代碼被編譯之后,Java虛擬機(jī)會(huì)利用monitorenter和monitorexit條字節(jié)碼指令來處理synchronized這個(gè)關(guān)鍵字。
根據(jù)虛擬機(jī)規(guī)范的要求,在執(zhí)行monitorenter指令時(shí),首先要嘗試獲取對(duì)象的鎖,如果這個(gè)對(duì)象沒有被鎖定,或者當(dāng)前線程已經(jīng)擁有了那個(gè)對(duì)象的鎖,把鎖的計(jì)數(shù)器加1,相應(yīng)地,在執(zhí)行monitorexit指令時(shí)會(huì)將鎖計(jì)數(shù)器減1,當(dāng)計(jì)數(shù)器為0時(shí),鎖就會(huì)被釋放。如果獲取對(duì)象鎖失敗,那當(dāng)前線程就要阻塞等待,直到對(duì)象鎖被另外一個(gè)線程釋放為止。
關(guān)于monitorenter和monitorexit,有兩點(diǎn)是要特別注意的:
1、synchronized同步塊對(duì)同一條線程來說是可重入的,不會(huì)出現(xiàn)把自己鎖死的問題
2、同步塊在已進(jìn)入的線程執(zhí)行完之前,會(huì)阻塞后面其它線程的進(jìn)入
因?yàn)镴ava的線程是映射到操作系統(tǒng)的原生線程之上的,如果要阻塞或者喚醒一個(gè)線程,都需要操作系統(tǒng)來幫忙完成,這就需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)中,因此狀態(tài)轉(zhuǎn)換需要耗費(fèi)很多的處理器時(shí)間,對(duì)于代碼簡(jiǎn)單的同步塊,狀態(tài)轉(zhuǎn)換消耗的時(shí)間有可能比用戶代碼執(zhí)行的時(shí)間還長(zhǎng),所以synchronized是Java語言中一個(gè)重量級(jí)(Heavyweight)鎖,有經(jīng)驗(yàn)的程序員都會(huì)在確實(shí)必要的情況下才使用這種操作。
順便看一下HotSpot虛擬機(jī)對(duì)象頭Mark Word:



看到有一個(gè)重量級(jí)鎖定,指的就是重量級(jí)鎖。

volatile的實(shí)現(xiàn)

對(duì)于volatile關(guān)鍵字,一個(gè)被volatile關(guān)鍵字修飾的變量,在生成匯編語言之后,大致會(huì)多出這么一條指令:

0x01a3de24:lock addl $0x0,(%esp)      ;...f0830424 00

這個(gè)操作相當(dāng)于是一個(gè)內(nèi)存屏障,只有一個(gè)CPU訪問內(nèi)存時(shí),并不需要內(nèi)存屏障;但如果有兩個(gè)或者更多CPU訪問同一塊內(nèi)存時(shí),且其中一個(gè)在觀測(cè)另外一個(gè),就需要內(nèi)存屏障來保證一致性了。這句指令中的"addl $0x0,(%esp)"(把esp寄存器的值加0)顯然是一個(gè)空操作(采用這個(gè)空操作而不是空指令nop是因?yàn)镮A32手冊(cè)規(guī)定lock前綴不允許配合nop指令使用),關(guān)鍵在于lock前綴,查詢IA32手冊(cè),它的作用是使得本CPU的Cache寫入了內(nèi)存,該寫入動(dòng)作也會(huì)引起別的CPU或者別的內(nèi)核無效化其Cache,這種操作相當(dāng)于對(duì)Cache中的變量做了一次"store和write"操作,所以通過這樣一個(gè)空操作,可讓前面volatile變量的修改對(duì)其他CPU立即可見。

自旋鎖與自適應(yīng)自旋

互斥同步,對(duì)性能影響最大的是阻塞的實(shí)現(xiàn),掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核狀態(tài)完成,這些操作給系統(tǒng)的并發(fā)性能帶來了很大的壓力。同時(shí),虛擬機(jī)開發(fā)團(tuán)隊(duì)也注意到很多應(yīng)用上,共享數(shù)據(jù)的鎖定狀態(tài)只會(huì)持續(xù)很短的一段時(shí)間,為了這段時(shí)間去掛起和恢復(fù)線程并不值得。如果物理機(jī)上有一個(gè)以上的處理器,能讓兩個(gè)或兩個(gè)以上的線程同時(shí)并行執(zhí)行,我們就可以讓后面請(qǐng)求鎖的那個(gè)線程"稍等一下",但不放棄處理器的執(zhí)行時(shí)間,看看持有鎖的線程是否很快就會(huì)釋放鎖。為了讓線程等待,我們只需要讓線程執(zhí)行一個(gè)忙循環(huán)(自旋),這項(xiàng)技術(shù)就是所謂的自旋鎖。

在JDK1.4.2就已經(jīng)引入了自旋鎖,只不過默認(rèn)是關(guān)閉的。自旋不能代替阻塞,且先不說處理器數(shù)量的要求,自旋等待本身雖然避免了線程切換的開銷,但是它是要占據(jù)處理器時(shí)間的,因此如果鎖被占用的時(shí)間很短,自旋等待的效果就非常好;反之,如果鎖被占用的時(shí)間很長(zhǎng),那么自旋的線程只會(huì)白白消耗處理器資源,而不會(huì)做任何有用的工作,反而會(huì)帶來性能上的浪費(fèi)。因此自選等待必須有一定的限度,如果自旋超過了限定的次數(shù)仍然沒有成功獲得鎖,就應(yīng)當(dāng)使用傳統(tǒng)的方式去掛起線程了,自旋次數(shù)的默認(rèn)值是10。

在JDK1.6之后引入了自適應(yīng)的自旋鎖。自適應(yīng)意味著自旋的時(shí)間不再固定了,而是由前一次在同一個(gè)鎖上自旋的時(shí)間以及鎖的擁有者的狀態(tài)來決定。如果在同一個(gè)鎖對(duì)象上,自旋等待剛剛獲得過鎖,并且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也很有可能再次成功,進(jìn)而它將允許自旋等待持續(xù)相對(duì)更長(zhǎng)的時(shí)間,比如100個(gè)循環(huán)。另外如果對(duì)于某一個(gè)鎖,自旋很少成功獲得過,那么在以后要獲得這個(gè)鎖時(shí)將可能忽略掉自旋過程,以避免浪費(fèi)處理器資源。有了自適應(yīng)自旋,隨著程序運(yùn)行和性能監(jiān)控信息的不斷完善,虛擬機(jī)對(duì)程序鎖的狀況預(yù)測(cè)就會(huì)越來越準(zhǔn)確。

鎖消除

鎖消除是指虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí),對(duì)一些代碼上要求同步,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除。鎖消除的主要判定依據(jù)來源于逃逸分析的支持,如果判斷在一段代碼中,堆上所有數(shù)據(jù)都不會(huì)逃逸出去從而被其他線程訪問到,那就可以把它們當(dāng)做棧上數(shù)據(jù)對(duì)待,認(rèn)為它們是線程私有的,同步加鎖自然無需進(jìn)行。

鎖粗化

原則上,我們?cè)诰帉懘a的時(shí)候,總是推薦將同步塊的作用范圍限制得盡量小----只在共享數(shù)據(jù)的實(shí)際作用域中才進(jìn)行同步,這樣是為了使得需要同步的操作數(shù)盡可能變小,如果存在鎖競(jìng)爭(zhēng),那等待鎖的線程也能盡快拿到鎖。
大部分情況下,上面的原則都是正確的,但是如果一系列的連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體中的,那即使沒有線程競(jìng)爭(zhēng),頻繁地進(jìn)行互斥同步操作也會(huì)導(dǎo)致不必要的性能損耗。
如果這么說不夠直觀,那么想想某段代碼反復(fù)使用StringBuffer的append方法拼接字符串的例子吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容