本文翻譯自How the Java virtual machine performs thread synchronization幼驶,內(nèi)容略有刪改
前言
所有的 Java 程序都會(huì)被翻譯為包含字節(jié)碼的 class 文件,字節(jié)碼是 JVM 的機(jī)器語言畔咧。這篇文章將闡述 JVM 是如何處理線程同步以及相關(guān)的字節(jié)碼袖裕。
線程和共享數(shù)據(jù)
Java 的一個(gè)優(yōu)點(diǎn)就是在語言層面支持多線程触徐,這種支持集中在協(xié)調(diào)多線程對數(shù)據(jù)的訪問上明垢。
JVM 將運(yùn)行時(shí)數(shù)據(jù)劃分為幾個(gè)區(qū)域:一個(gè)或多個(gè)棧神得,一個(gè)堆,一個(gè)方法區(qū)饥侵。
在 JVM 中鸵赫,每個(gè)線程擁有一個(gè)棧,其他線程無法訪問躏升,里面的數(shù)據(jù)包括:局部變量辩棒,函數(shù)參數(shù),線程調(diào)用的方法的返回值膨疏。棧里面的數(shù)據(jù)只包含原生數(shù)據(jù)類型和對象引用一睁。在 JVM 中,不可能將實(shí)際對象的拷貝放入棧佃却。所有對象都在堆里面者吁。
JVM 只有一個(gè)堆,所有線程都共享它饲帅。堆中只包含對象复凳,把單獨(dú)的原生類型或者對象引用放入堆也是不可能的,除非它們是對象的一部分灶泵。數(shù)組也在堆中育八,包括原生類型的數(shù)組,因?yàn)樵?Java 中丘逸,數(shù)組也是對象单鹿。
除了棧和堆,另一個(gè)存放數(shù)據(jù)的區(qū)域就是方法區(qū)了深纲,它包含程序中使用到的所有類(靜態(tài))變量仲锄。方法區(qū)類似于棧,也只包含原生類型和對象引用湃鹊,但是又跟棧不同儒喊,方法區(qū)中類變量是線程共享的。
對象鎖和類鎖
正如前面所說币呵,JVM 中的兩個(gè)區(qū)域包含線程共享的數(shù)據(jù)怀愧,分別是:
- 堆:包含所有對象
- 方法區(qū):包含所有類變量
如果多個(gè)線程需要同時(shí)使用同一個(gè)對象或者類變量,它們對數(shù)據(jù)的訪問必須被恰當(dāng)?shù)乜刂朴嘤7駝t芯义,程序會(huì)產(chǎn)生不可預(yù)測的行為。
為了協(xié)調(diào)多個(gè)線程對共享數(shù)據(jù)的訪問妻柒,JVM 給每個(gè)對象和類關(guān)聯(lián)了一個(gè)鎖扛拨。鎖就像是任意時(shí)間點(diǎn)只有一個(gè)線程能夠擁有的特權(quán)。如果一個(gè)線程想要鎖住一個(gè)特定的對象或者類举塔,它需要向 JVM 請求鎖绑警。線程向 JVM 請求鎖之后求泰,可能很快就拿到,或者過一會(huì)就拿到计盒,也可能永遠(yuǎn)拿不到渴频。當(dāng)線程不需要鎖之后,它把鎖還給 JVM北启。如果其他線程需要這個(gè)鎖卜朗,JVM 會(huì)交給該線程。
類鎖的實(shí)現(xiàn)其實(shí)跟對象鎖是一樣的暖庄。當(dāng) JVM 加載類文件的時(shí)候聊替,它會(huì)創(chuàng)建一個(gè)對應(yīng)類java.lang.Class
對象。當(dāng)你鎖住一個(gè)類的時(shí)候培廓,你實(shí)際上是鎖住了這個(gè)類的Class
對象。
線程訪問對象實(shí)例或者類變量的時(shí)候不需要獲取鎖春叫。但是如果一個(gè)線程獲取了一個(gè)鎖肩钠,其他線程不能訪問被鎖住的數(shù)據(jù),直到擁有鎖的線程釋放它暂殖。
管程
JVM 使用鎖和管程協(xié)作价匠。管程監(jiān)視一段代碼,保證一個(gè)時(shí)間點(diǎn)內(nèi)只有一個(gè)線程能執(zhí)行這段代碼呛每。
每個(gè)管程與一個(gè)對象引用關(guān)聯(lián)踩窖。當(dāng)線程到達(dá)管程監(jiān)視代碼段的第一條指令時(shí),線程必須獲取關(guān)聯(lián)對象的鎖晨横。線程不能執(zhí)行這段代碼直到它得到了鎖洋腮。一旦它得到了鎖,線程可以進(jìn)入被保護(hù)的代碼段手形。
當(dāng)線程離開被保護(hù)的代碼塊啥供,不管是如何離開的,它都會(huì)釋放關(guān)聯(lián)對象的鎖库糠。
多次鎖定
一個(gè)線程被允許鎖定一個(gè)對象多次伙狐。對于每個(gè)對象,JVM 維護(hù)了一個(gè)鎖的計(jì)數(shù)器瞬欧。沒有被鎖的對象計(jì)數(shù)為 0贷屎。當(dāng)一個(gè)線程第一次獲取鎖,計(jì)數(shù)器自增變?yōu)?1艘虎。每次這個(gè)線程(已經(jīng)得到鎖的線程)請求同一個(gè)對象的鎖唉侄,計(jì)數(shù)器都會(huì)自增。每次線程釋放鎖顷帖,計(jì)數(shù)器都會(huì)自減美旧。當(dāng)計(jì)數(shù)器變?yōu)?0 時(shí)渤滞,鎖才被釋放,可以給別的線程使用榴嗅。
同步塊
在 Java 語言的術(shù)語中妄呕,協(xié)調(diào)多個(gè)線程訪問共享數(shù)據(jù)被稱為同步(synchronization)。Java 提供了兩種內(nèi)建的方式來同步對數(shù)據(jù)的訪問:
- 同步語句
- 同步方法
同步語句
為了創(chuàng)建同步語句嗽测,你需要使用synchronized
關(guān)鍵字绪励,括號里面是同步的對象引用,如下所示:
class KitchenSync {
private int[] intArray = new int[10];
void reverseOrder() {
synchronized (this) {
int halfWay = intArray.length / 2;
for (int i = 0; i < halfWay; ++i) {
int upperIndex = intArray.length - 1 - i;
int save = intArray[upperIndex];
intArray[upperIndex] = intArray[i];
intArray[i] = save;
}
}
}
}
在上面的例子中唠粥,被同步塊包含的語句不會(huì)被執(zhí)行疏魏,直到線程得到this
引用的對象鎖。如果不是鎖住this
引用晤愧,而是鎖住其他對象大莫,在線程執(zhí)行同步塊語句之前,它需要獲得該對象的鎖官份。
有兩個(gè)字節(jié)碼monitorenter
和monitorexit
只厘,被用來同步方法中的同步塊。
字節(jié)碼 | 操作數(shù) | 描述 |
---|---|---|
monitorenter | 無 | 取出對象引用舅巷,請求與對象引用關(guān)聯(lián)的鎖 |
monitorexit | 無 | 取出對象引用羔味,釋放與對象引用關(guān)聯(lián)的鎖 |
當(dāng)monitorenter
被 JVM 執(zhí)行時(shí),它請求棧頂對象引用關(guān)聯(lián)的鎖钠右。如果該線程已經(jīng)擁有該對象的鎖赋元,計(jì)數(shù)器自增。每次monitorexit
被執(zhí)行飒房,計(jì)數(shù)器自減搁凸。當(dāng)計(jì)數(shù)器變?yōu)?0 時(shí),該鎖被釋放情屹。
注意:當(dāng)同步塊中拋出異常時(shí)坪仇,catch
語句保證對象鎖被釋放。不管同步塊是如何退出的垃你,JVM 保證線程會(huì)釋放鎖椅文。
同步方法
為了同步整個(gè)方法,你只需要在方法聲明前面加上synchronized
關(guān)鍵字惜颇。
class HeatSync {
private int[] intArray = new int[10];
synchronized void reverseOrder() {
int halfWay = intArray.length / 2;
for (int i = 0; i < halfWay; ++i) {
int upperIndex = intArray.length - 1 - i;
int save = intArray[upperIndex];
intArray[upperIndex] = intArray[i];
intArray[i] = save;
}
}
}
JVM 不會(huì)使用特殊的字節(jié)碼來調(diào)用同步方法皆刺。當(dāng) JVM 解析方法的符號引用時(shí),它會(huì)判斷方法是不是同步的凌摄。如果是羡蛾,JVM 要求線程在調(diào)用之前請求鎖。對于實(shí)例方法锨亏,JVM 要求得到該實(shí)例對象的鎖痴怨。對于類方法忙干,JVM 要求得到類鎖。在同步方法完成之后浪藻,不管它是正常返回還是拋出異常捐迫,鎖都會(huì)被釋放。