深入理解多線程(五)—— Java虛擬機(jī)的鎖優(yōu)化技術(shù)

原文轉(zhuǎn)載:http://www.hollischuang.com/archives/2344

前情提要

通過前面幾篇文章,我們已經(jīng)知道:

1、同步方法通過ACC_SYNCHRONIZED關(guān)鍵字隱式的對(duì)方法進(jìn)行加鎖。當(dāng)線程要執(zhí)行的方法被標(biāo)注上ACC_SYNCHRONIZED時(shí),需要先獲得鎖才能執(zhí)行該方法。《深入理解多線程(一)——Synchronized的實(shí)現(xiàn)原理

2、同步代碼塊通過monitorentermonitorexit執(zhí)行來進(jìn)行加鎖。當(dāng)線程執(zhí)行到monitorenter的時(shí)候要先獲得所鎖,才能執(zhí)行后面的方法。當(dāng)線程執(zhí)行到monitorexit的時(shí)候則要釋放鎖。《深入理解多線程(四)—— Moniter的實(shí)現(xiàn)原理

3、在HotSpot虛擬機(jī)中,使用oop-klass模型來表示對(duì)象。每一個(gè)Java類,在被JVM加載的時(shí)候,JVM會(huì)給這個(gè)類創(chuàng)建一個(gè)instanceKlass,保存在方法區(qū),用來在JVM層表示該Java類。當(dāng)我們在Java代碼中,使用new創(chuàng)建一個(gè)對(duì)象的時(shí)候,JVM會(huì)創(chuàng)建一個(gè)instanceOopDesc對(duì)象,這個(gè)對(duì)象中包含了對(duì)象頭以及實(shí)例數(shù)據(jù)。《深入理解多線程(二)—— Java的對(duì)象模型

4、對(duì)象頭中主要包含了GC分代年齡、鎖狀態(tài)標(biāo)記、哈希碼、epoch等信息。對(duì)象的狀態(tài)一共有五種,分別是無鎖態(tài)、輕量級(jí)鎖、重量級(jí)鎖、GC標(biāo)記和偏向鎖。《深入理解多線程(三)—— Java的對(duì)象頭

在上一篇文章的最后,我們說過,事實(shí)上,只有在JDK1.6之前,synchronized的實(shí)現(xiàn)才會(huì)直接調(diào)用ObjectMonitorenterexit,這種鎖被稱之為重量級(jí)鎖。

高效并發(fā)是從JDK 1.5 到 JDK 1.6的一個(gè)重要改進(jìn),HotSpot虛擬機(jī)開發(fā)團(tuán)隊(duì)在這個(gè)版本中花費(fèi)了很大的精力去對(duì)Java中的鎖進(jìn)行優(yōu)化,如適應(yīng)性自旋、鎖消除、鎖粗化、輕量級(jí)鎖和偏向鎖等。這些技術(shù)都是為了在線程之間更高效的共享數(shù)據(jù),以及解決競爭問題。

本文,主要先來介紹一下自旋、鎖消除以及鎖粗化等技術(shù)。

這里簡單說明一下,本文要介紹的這幾個(gè)概念,以及后面要介紹的輕量級(jí)鎖和偏向鎖,其實(shí)對(duì)于使用他的開發(fā)者來說是屏蔽掉了的,也就是說,作為一個(gè)Java開發(fā),你只需要知道你想在加鎖的時(shí)候使用synchronized就可以了,具體的鎖的優(yōu)化是虛擬機(jī)根據(jù)競爭情況自行決定的。

也就是說,在JDK 1.5 以后,我們即將介紹的這些概念,都被封裝在synchronized中了。

線程狀態(tài)

要想把鎖說清楚,一個(gè)重要的概念不得不提,那就是線程和線程的狀態(tài)。鎖和線程的關(guān)系是怎樣的呢,舉個(gè)簡單的例子你就明白了。

比如,你今天要去銀行辦業(yè)務(wù),你到了銀行之后,要先取一個(gè)號(hào),然后你坐在休息區(qū)等待叫號(hào),過段時(shí)間,廣播叫到你的號(hào)碼之后,會(huì)告訴你去哪個(gè)柜臺(tái)辦理業(yè)務(wù),這時(shí),你拿著你手里的號(hào)碼,去到對(duì)應(yīng)的柜臺(tái),找相應(yīng)的柜員開始辦理業(yè)務(wù)。當(dāng)你辦理業(yè)務(wù)的時(shí)候,這個(gè)柜臺(tái)和柜臺(tái)后面的柜員只能為你自己服務(wù)。當(dāng)你辦完業(yè)務(wù)離開之后,廣播再喊其他的顧客前來辦理業(yè)務(wù)。

這個(gè)例子中,每個(gè)顧客是一個(gè)線程。 柜臺(tái)前面的那把椅子,就是。 柜臺(tái)后面的柜員,就是共享資源。 你發(fā)現(xiàn)無法直接辦理業(yè)務(wù),要取號(hào)等待的過程叫做阻塞。 當(dāng)你聽到叫你的號(hào)碼的時(shí)候,你起身去辦業(yè)務(wù),這就是喚醒。 當(dāng)你坐在椅子上開始辦理業(yè)務(wù)的時(shí)候,你就獲得鎖。 當(dāng)你辦完業(yè)務(wù)離開的時(shí)候,你就釋放鎖

對(duì)于線程來說,一共有五種狀態(tài),分別為:初始狀態(tài)(New) 、就緒狀態(tài)(Runnable) 、運(yùn)行狀態(tài)(Running) 、阻塞狀態(tài)(Blocked) 和死亡狀態(tài)(Dead) 。

自旋鎖

前一篇文章中,我們介紹的synchronized的實(shí)現(xiàn)方式中使用Monitor進(jìn)行加鎖,這是一種互斥鎖,為了表示他對(duì)性能的影響我們稱之為重量級(jí)鎖。

這種互斥鎖在互斥同步上對(duì)性能的影響很大,Java的線程是映射到操作系統(tǒng)原生線程之上的,如果要阻塞或喚醒一個(gè)線程就需要操作系統(tǒng)的幫忙,這就要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài),因此狀態(tài)轉(zhuǎn)換需要花費(fèi)很多的處理器時(shí)間。

就像去銀行辦業(yè)務(wù)的例子,當(dāng)你來到銀行,發(fā)現(xiàn)柜臺(tái)前面都有人的時(shí)候,你需要取一個(gè)號(hào),然后再去等待區(qū)等待,一直等待被叫號(hào)。這個(gè)過程是比較浪費(fèi)時(shí)間的,那么有沒有什么辦法改進(jìn)呢?

有一種比較好的設(shè)計(jì),那就是銀行提供自動(dòng)取款機(jī),當(dāng)你去銀行取款的時(shí)候,你不需要取號(hào),不需要去休息區(qū)等待叫號(hào),你只需要找到一臺(tái)取款機(jī),排在其他人后面等待取款就行了。

之所以能這樣做,是因?yàn)槿】畹倪@個(gè)過程相比較之下是比較節(jié)省時(shí)間的。如果所有人去銀行都只取款,或者辦理業(yè)務(wù)的時(shí)間都很短的話,那也就可以不需要取號(hào),不需要去單獨(dú)的休息區(qū),不需要聽叫號(hào),也不需要再跑到對(duì)應(yīng)的柜臺(tái)了。

而,在程序中,Java虛擬機(jī)的開發(fā)工程師們在分析過大量數(shù)據(jù)后發(fā)現(xiàn):共享數(shù)據(jù)的鎖定狀態(tài)一般只會(huì)持續(xù)很短的一段時(shí)間,為了這段時(shí)間去掛起和恢復(fù)線程其實(shí)并不值得。

如果物理機(jī)上有多個(gè)處理器,可以讓多個(gè)線程同時(shí)執(zhí)行的話。我們就可以讓后面來的線程“稍微等一下”,但是并不放棄處理器的執(zhí)行時(shí)間,看看持有鎖的線程會(huì)不會(huì)很快釋放鎖。這個(gè)“稍微等一下”的過程就是自旋。

自旋鎖在JDK 1.4中已經(jīng)引入,在JDK 1.6中默認(rèn)開啟。

很多人在對(duì)于自旋鎖的概念不清楚的時(shí)候可能會(huì)有以下疑問:這么聽上去,自旋鎖好像和阻塞鎖沒啥區(qū)別,反正都是等著嘛。

  • 對(duì)于去銀行取錢的你來說,站在取款機(jī)面前等待和去休息區(qū)等待叫號(hào)有一個(gè)很大的區(qū)別:

    • 那就是如果你在休息區(qū)等待,這段時(shí)間你什么都不需要管,隨意做自己的事情,等著被喚醒就行了。

    • 如果你在取款機(jī)面前等待,那么你需要時(shí)刻關(guān)注自己前面還有沒有人,因?yàn)闆]人會(huì)喚醒你。

    • 很明顯,這種直接去取款機(jī)前面排隊(duì)取款的效率是比較高。

所以呢,自旋鎖和阻塞鎖最大的區(qū)別就是,到底要不要放棄處理器的執(zhí)行時(shí)間。對(duì)于阻塞鎖和自旋鎖來說,都是要等待獲得共享資源。但是阻塞鎖是放棄了CPU時(shí)間,進(jìn)入了等待區(qū),等待被喚醒。而自旋鎖是一直“自旋”在那里,時(shí)刻的檢查共享資源是否可以被訪問。

由于自旋鎖只是將當(dāng)前線程不停地執(zhí)行循環(huán)體,不進(jìn)行線程狀態(tài)的改變,所以響應(yīng)速度更快。但當(dāng)線程數(shù)不停增加時(shí),性能下降明顯,因?yàn)槊總€(gè)線程都需要執(zhí)行,占用CPU時(shí)間。如果線程競爭不激烈,并且保持鎖的時(shí)間段。適合使用自旋鎖。

鎖消除

除了自旋鎖之后,JDK中還有一種鎖的優(yōu)化被稱之為鎖消除。還拿去銀行取錢的例子說。

你去銀行取錢,所有情況下都需要取號(hào),并且等待嗎?其實(shí)是不用的,當(dāng)銀行辦理業(yè)務(wù)的人不多的時(shí)候,可能根本不需要取號(hào),直接走到柜臺(tái)前面辦理業(yè)務(wù)就好了。

能這么做的前提是,沒有人和你搶著辦業(yè)務(wù)。

上面的這種例子,在鎖優(yōu)化中被稱作“鎖消除”,是JIT編譯器對(duì)內(nèi)部鎖的具體實(shí)現(xiàn)所做的一種優(yōu)化。

在動(dòng)態(tài)編譯同步塊的時(shí)候,JIT編譯器可以借助一種被稱為逃逸分析(Escape Analysis)的技術(shù)來判斷同步塊所使用的鎖對(duì)象是否只能夠被一個(gè)線程訪問而沒有被發(fā)布到其他線程。

如果同步塊所使用的鎖對(duì)象通過這種分析被證實(shí)只能夠被一個(gè)線程訪問,那么JIT編譯器在編譯這個(gè)同步塊的時(shí)候就會(huì)取消對(duì)這部分代碼的同步。

如以下代碼:

public void f() {
    Object hollis = new Object();
    synchronized(hollis) {
        System.out.println(hollis);
    }
}

代碼中對(duì)hollis這個(gè)對(duì)象進(jìn)行加鎖,但是hollis對(duì)象的生命周期只在f()方法中,并不會(huì)被其他線程所訪問到,所以在JIT編譯階段就會(huì)被優(yōu)化掉。優(yōu)化成:

public void f() {
    Object hollis = new Object();
    System.out.println(hollis);
}

這里,可能有讀者會(huì)質(zhì)疑了,代碼是程序員自己寫的,程序員難道沒有能力判斷要不要加鎖嗎?就像以上代碼,完全沒必要加鎖,有經(jīng)驗(yàn)的開發(fā)者一眼就能看的出來的。其實(shí)道理是這樣,但是還是有可能有疏忽,比如我們經(jīng)常在代碼中使用StringBuffer作為局部變量,而StringBuffer中的append是線程安全的,有synchronized修飾的,這種情況開發(fā)者可能會(huì)忽略。這時(shí)候,JIT就可以幫忙優(yōu)化,進(jìn)行鎖消除。

了解我的朋友都知道,一般到這個(gè)時(shí)候,我就會(huì)開始反編譯,然后拿出反編譯之后的代碼來證明鎖優(yōu)化確實(shí)存在。

但是,之前很多例子之所以可以用反編譯工具,是因?yàn)槟切皟?yōu)化”,如語法糖等,是在javac編譯階段發(fā)生的,并不是在JIT編譯階段發(fā)生的。而鎖優(yōu)化,是JIT編譯器的功能,所以,無法使用現(xiàn)有的反編譯工具查看具體的優(yōu)化結(jié)果。(關(guān)于javac編譯和JIT編譯的關(guān)系和區(qū)別,我在我的知識(shí)星球中單獨(dú)發(fā)了一篇文章介紹。)

但是,如果讀者感興趣,還是可以看的,只是會(huì)復(fù)雜一點(diǎn),首先你要自己build一個(gè)fasttest版本的jdk,然后在使用java命令對(duì).class文件進(jìn)行執(zhí)行的時(shí)候加上-XX:+PrintEliminateLocks參數(shù)。而且jdk的模式還必須是server模式。

總之,讀者只需要知道,在使用synchronized的時(shí)候,如果JIT經(jīng)過逃逸分析之后發(fā)現(xiàn)并無線程安全問題的話,就會(huì)做鎖消除。

鎖粗化

很多人都知道,在代碼中,需要加鎖的時(shí)候,我們提倡盡量減小鎖的粒度,這樣可以避免不必要的阻塞。

這也是很多人原因是用同步代碼塊來代替同步方法的原因,因?yàn)橥牧6葧?huì)更小一些,這其實(shí)是很有道理的。

還是我們?nèi)ャy行柜臺(tái)辦業(yè)務(wù),最高效的方式是你坐在柜臺(tái)前面的時(shí)候,只辦和銀行相關(guān)的事情。如果這個(gè)時(shí)候,你拿出手機(jī),接打幾個(gè)電話,問朋友要往哪個(gè)賬戶里面打錢,這就很浪費(fèi)時(shí)間了。最好的做法肯定是提前準(zhǔn)備好相關(guān)資料,在辦理業(yè)務(wù)時(shí)直接辦理就好了。

加鎖也一樣,把無關(guān)的準(zhǔn)備工作放到鎖外面,鎖內(nèi)部只處理和并發(fā)相關(guān)的內(nèi)容。這樣有助于提高效率。

那么,這和鎖粗化有什么關(guān)系呢?可以說,大部分情況下,減小鎖的粒度是很正確的做法,只有一種特殊的情況下,會(huì)發(fā)生一種叫做鎖粗化的優(yōu)化。

就像你去銀行辦業(yè)務(wù),你為了減少每次辦理業(yè)務(wù)的時(shí)間,你把要辦的五個(gè)業(yè)務(wù)分成五次去辦理,這反而適得其反了。因?yàn)檫@平白的增加了很多你重新取號(hào)、排隊(duì)、被喚醒的時(shí)間。

如果在一段代碼中連續(xù)的對(duì)同一個(gè)對(duì)象反復(fù)加鎖解鎖,其實(shí)是相對(duì)耗費(fèi)資源的,這種情況可以適當(dāng)放寬加鎖的范圍,減少性能消耗。

當(dāng)JIT發(fā)現(xiàn)一系列連續(xù)的操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖,甚至加鎖操作出現(xiàn)在循環(huán)體中的時(shí)候,會(huì)將加鎖同步的范圍擴(kuò)散(粗化)到整個(gè)操作序列的外部。

如以下代碼:

for(int i=0;i<100000;i++){  
    synchronized(this){  
        do();  
}  

會(huì)被粗化成:

synchronized(this){  
    for(int i=0;i<100000;i++){  
        do();  
}  

這其實(shí)和我們要求的減小鎖粒度并不沖突。減小鎖粒度強(qiáng)調(diào)的是不要在銀行柜臺(tái)前做準(zhǔn)備工作以及和辦理業(yè)務(wù)無關(guān)的事情。而鎖粗化建議的是,同一個(gè)人,要辦理多個(gè)業(yè)務(wù)的時(shí)候,可以在同一個(gè)窗口一次性辦完,而不是多次取號(hào)多次辦理。

總結(jié)

自Java 6/Java 7開始,Java虛擬機(jī)對(duì)內(nèi)部鎖的實(shí)現(xiàn)進(jìn)行了一些優(yōu)化。這些優(yōu)化主要包括鎖消除(Lock Elision)、鎖粗化(Lock Coarsening)、偏向鎖(Biased Locking)以及適應(yīng)性自旋鎖(Adaptive Locking)。這些優(yōu)化僅在Java虛擬機(jī)server模式下起作用(即運(yùn)行Java程序時(shí)我們可能需要在命令行中指定Java虛擬機(jī)參數(shù)“-server”以開啟這些優(yōu)化)。

本文主要介紹了自旋鎖、鎖粗化和鎖消除的概念。在JIT編譯過程中,虛擬機(jī)會(huì)根據(jù)情況使用這三種技術(shù)對(duì)鎖進(jìn)行優(yōu)化,目的是減少鎖的競爭,提升性能。

(全部五篇完結(jié))

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容