所有Java程序都會(huì)被編譯成類文件,這些文件包含字節(jié)碼,即Java虛擬機(jī)的機(jī)器語(yǔ)言。 本文介紹了Java虛擬機(jī)如何處理線程同步,包括相關(guān)的字節(jié)碼。本文介紹了與線程同步相關(guān)的兩個(gè)機(jī)器碼,這兩個(gè)機(jī)器碼用于線程同步進(jìn)入和推出監(jiān)視器(enter monitor/exit monitor)。
線程和共享數(shù)據(jù)
Java的優(yōu)勢(shì)之一是在語(yǔ)言級(jí)別支持多線程。這種支持主要集中在協(xié)調(diào)對(duì)多個(gè)線程共享的數(shù)據(jù)的訪問(wèn)。
JVM將正在運(yùn)行的Java應(yīng)用程序的數(shù)據(jù)組織成幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)域:一個(gè)或者多個(gè)線程棧、一個(gè)堆和一個(gè)方法區(qū)。
在JVM內(nèi)部,每一個(gè)線程被分配一個(gè)棧,用于存放此線程單獨(dú)使用的數(shù)據(jù),包括本地變量表、參數(shù)和該線程調(diào)用的每一個(gè)方法的返回值。棧中的數(shù)據(jù)僅僅包括原始數(shù)據(jù)類型和對(duì)象引用。在JVM中,不可能將實(shí)際對(duì)象放在線程棧上,所有的對(duì)象都在堆上。
在JVM上只有一個(gè)堆,所有線程共享此區(qū)域。堆上存放了所有的對(duì)象。無(wú)法在堆上放置單獨(dú)的原語(yǔ)類型或?qū)ο笠?-除非這些是其他對(duì)象的組成部分。數(shù)組也是在堆里的,包括原始類型的數(shù)組,但是在Java中,數(shù)組本身也是對(duì)象。
除了棧和堆,在JVM中存放數(shù)據(jù)的還有方法區(qū),其包含所有的類(class)變量和靜態(tài)變量。方法區(qū)和棧很相似,也僅僅包括原始數(shù)據(jù)類型和對(duì)象引用。但是類變量是所有線程共享的。
對(duì)象級(jí)鎖和類級(jí)鎖
通過(guò)上面的描述,JVM中有兩塊內(nèi)存區(qū)域包含了所有線程能共享訪問(wèn)的數(shù)據(jù),它們是:
- 堆,包含了所有的對(duì)象
- 方法區(qū),包含了所有的類變量和靜態(tài)變量
如果多個(gè)線程需要并發(fā)的使用同一個(gè)對(duì)象或者類變量,必須要對(duì)他們使用數(shù)據(jù)做合適的管理。否則,程序?qū)⒊霈F(xiàn)不可預(yù)測(cè)的結(jié)果。
為了協(xié)調(diào)多個(gè)線程之間的共享數(shù)據(jù)訪問(wèn),JVM將鎖與每個(gè)對(duì)象和類變量關(guān)聯(lián)。 鎖就像特權(quán),一次只能有一個(gè)線程“擁有”。 如果線程想要鎖定特定的對(duì)象或類,它將向JVM申請(qǐng)。當(dāng)線程不再需要鎖時(shí),它將鎖還給JVM。 如果另一個(gè)線程請(qǐng)求了相同的鎖,則JVM將鎖傳遞給該線程。
類級(jí)別鎖的實(shí)現(xiàn)實(shí)際上和對(duì)象鎖一樣。當(dāng)JVM加載一個(gè)class文件時(shí),它創(chuàng)建了一個(gè)java.lang.Class的實(shí)例。當(dāng)你鎖定一個(gè)類時(shí),你實(shí)際上鎖定的時(shí)那個(gè)類的Class對(duì)象。
線程無(wú)需獲取鎖即可訪問(wèn)實(shí)例或類變量。 但是,如果某個(gè)線程確實(shí)獲得了鎖定,則其他線程無(wú)法訪問(wèn)鎖定的數(shù)據(jù),直到擁有該鎖定的線程將其釋放為止。
監(jiān)視器
JVM借助于監(jiān)視器來(lái)使用鎖。監(jiān)視器基本上是一個(gè)守護(hù)者,因?yàn)樗O(jiān)視一串代碼序列,確保一次只有一個(gè)線程執(zhí)行代碼。
每個(gè)監(jiān)視器都與一個(gè)對(duì)象引用關(guān)聯(lián)。 當(dāng)線程到達(dá)監(jiān)視器監(jiān)視下的代碼塊中的第一條指令時(shí),該線程必須獲得對(duì)引用對(duì)象的鎖定。 線程獲得鎖之前,不允許執(zhí)行代碼。 一旦獲得了鎖,線程便進(jìn)入受保護(hù)的代碼塊。當(dāng)線程離開(kāi)該塊時(shí),無(wú)論它如何離開(kāi)該塊,它都會(huì)釋放關(guān)聯(lián)對(duì)象上的鎖。
多重鎖
允許單個(gè)線程多次鎖定同一對(duì)象。 對(duì)于每個(gè)對(duì)象,JVM都會(huì)對(duì)該對(duì)象被鎖定的次數(shù)進(jìn)行計(jì)數(shù)。 一個(gè)未鎖定的對(duì)象的計(jì)數(shù)為零。 當(dāng)線程首次獲取鎖時(shí),計(jì)數(shù)將增加為一。 線程每次在同一對(duì)象上獲取鎖時(shí),計(jì)數(shù)都會(huì)增加。 每次線程釋放鎖定時(shí),計(jì)數(shù)都會(huì)減少。 當(dāng)計(jì)數(shù)達(dá)到零時(shí),將釋放該鎖并將其提供給其他線程。
同步塊
在Java語(yǔ)言術(shù)語(yǔ)中,將訪問(wèn)共享數(shù)據(jù)的多個(gè)線程的協(xié)調(diào)稱為同步。 該語(yǔ)言提供了兩種內(nèi)置的方法來(lái)同步對(duì)數(shù)據(jù)的訪問(wèn):使用同步語(yǔ)句或同步方法。
同步語(yǔ)句塊
通過(guò)synchronized關(guān)鍵字修飾包含有一個(gè)對(duì)象引用的表達(dá)式來(lái)創(chuàng)建一個(gè)同步語(yǔ)句,例如下面的代碼:
class SyncStatementsDemo{
private List<String> demoLst=new ArraysList<String>(10);
void syncMethod(){
synchronized(this){
if(demoLst!=null&&demoLst.size()>0){
demoLst[1]=demoLst[2];
}
}
}
}
在上述情況下,直到在當(dāng)前對(duì)象(this)上獲得鎖后,才會(huì)執(zhí)行同步塊中包含的語(yǔ)句。 如果表達(dá)式產(chǎn)生了對(duì)另一個(gè)對(duì)象的引用,而不是這個(gè)this引用,那么在線程繼續(xù)之前,將獲取與該對(duì)象相關(guān)聯(lián)的鎖。
兩個(gè)機(jī)器碼,monitorenter和monitorexit,被用于方法同步塊,解釋如下:
| 機(jī)器碼 | 描述 |
|---|---|
monitorenter |
彈出對(duì)象引用,獲取與該對(duì)象引用關(guān)聯(lián)的鎖 |
monitorexit |
彈出對(duì)象引用,釋放與該對(duì)象引用關(guān)聯(lián)的鎖 |
當(dāng)JVM遇到monitorenter時(shí),它將獲取堆棧上對(duì)象引用的對(duì)象的鎖。 如果線程已經(jīng)擁有該對(duì)象的鎖,則將增加一個(gè)計(jì)數(shù)。 每次對(duì)對(duì)象上的線程執(zhí)行monitorexit時(shí),計(jì)數(shù)都會(huì)減少。 當(dāng)計(jì)數(shù)達(dá)到零時(shí),將釋放監(jiān)視器。
請(qǐng)注意,即使同步塊內(nèi)發(fā)生異常,catch子句也可以確保鎖定的對(duì)象將被解鎖釋放。 無(wú)論同步塊如何退出,線程進(jìn)入該塊時(shí)所獲得的對(duì)象鎖定都將被釋放。
同步方法
僅僅需要在方法修飾符上加上synchronized就可以同步整個(gè)方法,如下:
class SyncStatementsDemo{
private List<String> demoLst=new ArraysList<String>(10);
synchronized void syncMethod(){
if(demoLst!=null&&demoLst.size()>0){
demoLst[1]=demoLst[2];
}
}
}
JVM不使用任何特殊的操作碼來(lái)調(diào)用同步方法或從同步方法返回。 當(dāng)JVM解析對(duì)方法的符號(hào)引用時(shí),它將確定該方法是否時(shí)同步的。 如果是,則JVM在調(diào)用該方法之前獲取一個(gè)鎖。 對(duì)于實(shí)例方法,JVM獲取與在其上調(diào)用該方法的對(duì)象關(guān)聯(lián)的鎖。 對(duì)于類方法,它獲取與該方法所屬的類關(guān)聯(lián)的鎖。 同步方法完成后,無(wú)論是通過(guò)返回還是方法拋出異常來(lái)完成,都將釋放該鎖。