理解Java對(duì)象頭與Monitor
在JVM中平绩,對(duì)象在內(nèi)存中的布局分為三塊區(qū)域:對(duì)象頭手素、實(shí)例數(shù)據(jù)和對(duì)齊填充快毛。如下:實(shí)例變量:存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息,如果是數(shù)組的實(shí)例部分還包括數(shù)組的長(zhǎng)度拯欧,這部分內(nèi)存按4字節(jié)對(duì)齊碉熄。
填充數(shù)據(jù):由于虛擬機(jī)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍桨武。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對(duì)齊锈津。
其中Mark Word在默認(rèn)情況下存儲(chǔ)著對(duì)象的HashCode倾哺、分代年齡、鎖標(biāo)記位等以下是32位JVM的Mark Word默認(rèn)存儲(chǔ)結(jié)構(gòu)
由于對(duì)象頭的信息是與對(duì)象自身定義的數(shù)據(jù)沒(méi)有關(guān)系的額外存儲(chǔ)成本,因此考慮到JVM的空間效率羞海,Mark Word 被設(shè)計(jì)成為一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)忌愚,以便存儲(chǔ)更多有效的數(shù)據(jù),它會(huì)根據(jù)對(duì)象本身的狀態(tài)復(fù)用自己的存儲(chǔ)空間却邓,如32位JVM下硕糊,除了上述列出的Mark Word默認(rèn)存儲(chǔ)結(jié)構(gòu)外,還有如下可能變化的結(jié)構(gòu):
其中輕量級(jí)鎖和偏向鎖是Java 6對(duì) synchronized 鎖進(jìn)行優(yōu)化后新增加的腊徙,稍后我們會(huì)簡(jiǎn)要分析癌幕。這里我們主要分析一下重量級(jí)鎖也就是通常說(shuō)synchronized的對(duì)象鎖,鎖標(biāo)識(shí)位為10昧穿,其中指針指向的是monitor對(duì)象(也稱為管程或監(jiān)視器鎖)的起始地址勺远。每個(gè)對(duì)象都存在著一個(gè) monitor 與之關(guān)聯(lián),對(duì)象與其 monitor 之間的關(guān)系有存在多種實(shí)現(xiàn)方式时鸵,如monitor可以與對(duì)象一起創(chuàng)建銷毀或當(dāng)線程試圖獲取對(duì)象鎖時(shí)自動(dòng)生成胶逢,但當(dāng)一個(gè) monitor 被某個(gè)線程持有后,它便處于鎖定狀態(tài)饰潜。 先來(lái)舉個(gè)例子初坠,然后我們?cè)谏显创a。我們可以把監(jiān)視器理解為包含一個(gè)特殊的房間的建筑物彭雾,這個(gè)特殊房間同一時(shí)刻只能有一個(gè)客人(線程)碟刺。這個(gè)房間中包含了一些數(shù)據(jù)和代碼。
如果一個(gè)顧客想要進(jìn)入這個(gè)特殊的房間薯酝,他首先需要在走廊(Entry Set)排隊(duì)等待半沽。調(diào)度器將基于某個(gè)標(biāo)準(zhǔn)(比如 FIFO)來(lái)選擇排隊(duì)的客戶進(jìn)入房間。如果吴菠,因?yàn)槟承┰蛘咛睿摽蛻魰簳r(shí)因?yàn)槠渌虑闊o(wú)法脫身(線程被掛起),那么他將被送到另外一間專門用來(lái)等待的房間(Wait Set)做葵,這個(gè)房間的可以可以在稍后再次進(jìn)入那件特殊的房間占哟。如上面所說(shuō),這個(gè)建筑屋中一共有三個(gè)場(chǎng)所酿矢。
總之榨乎,監(jiān)視器是一個(gè)用來(lái)監(jiān)視這些線程進(jìn)入特殊的房間的。他的義務(wù)是保證(同一時(shí)間)只有一個(gè)線程可以訪問(wèn)被保護(hù)的數(shù)據(jù)和代碼瘫筐。
Monitor其實(shí)是一種同步工具蜜暑,也可以說(shuō)是一種同步機(jī)制,它通常被描述為一個(gè)對(duì)象严肪,主要特點(diǎn)是:
- 對(duì)象的所有方法都被“互斥”的執(zhí)行史煎。好比一個(gè)Monitor只有一個(gè)運(yùn)行“許可”谦屑,任一個(gè)線程進(jìn)入任何一個(gè)方法都需要獲得這個(gè)“許可”,離開(kāi)時(shí)把許可歸還篇梭。
- 通常提供singal機(jī)制:允許正持有“許可”的線程暫時(shí)放棄“許可”氢橙,等待某個(gè)謂詞成真(條件變量),而條件成立后恬偷,當(dāng)前進(jìn)程可以“通知”正在等待這個(gè)條件變量的線程悍手,讓他可以重新去獲得運(yùn)行許可。
監(jiān)視器的實(shí)現(xiàn)
在Java虛擬機(jī)(HotSpot)中袍患,Monitor是基于C++實(shí)現(xiàn)的坦康,由ObjectMonitor實(shí)現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下:
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor中有幾個(gè)關(guān)鍵屬性:
_owner:指向持有ObjectMonitor對(duì)象的線程
_WaitSet:存放處于wait狀態(tài)的線程隊(duì)列
_EntryList:存放處于等待鎖block狀態(tài)的線程隊(duì)列
_recursions:鎖的重入次數(shù)
_count:用來(lái)記錄該線程獲取鎖的次數(shù)
當(dāng)多個(gè)線程同時(shí)訪問(wèn)一段同步代碼時(shí)诡延,首先會(huì)進(jìn)入_EntryList隊(duì)列中滞欠,當(dāng)某個(gè)線程獲取到對(duì)象的monitor后進(jìn)入_Owner區(qū)域并把monitor中的_owner變量設(shè)置為當(dāng)前線程,同時(shí)monitor中的計(jì)數(shù)器_count加1肆良。即獲得對(duì)象鎖筛璧。
若持有monitor的線程調(diào)用wait()方法,將釋放當(dāng)前持有的monitor惹恃,_owner變量恢復(fù)為null夭谤,_count自減1,同時(shí)該線程進(jìn)入_WaitSet集合中等待被喚醒巫糙。若當(dāng)前線程執(zhí)行完畢也將釋放monitor(鎖)并復(fù)位變量的值朗儒,以便其他線程進(jìn)入獲取monitor(鎖)。如下圖所示ObjectMonitor類中提供了幾個(gè)方法:
獲得鎖
void ATTR ObjectMonitor::enter(TRAPS) {
Thread * const Self = THREAD ;
void * cur ;
//通過(guò)CAS嘗試把monitor的`_owner`字段設(shè)置為當(dāng)前線程
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
//獲取鎖失敗
if (cur == NULL) { assert (_recursions == 0 , "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert OwnerIsThread == 1
return ;
}
//如果舊值和當(dāng)前線程一樣参淹,說(shuō)明當(dāng)前線程已經(jīng)持有鎖醉锄,此次為重入,_recursions自增承二,并獲得鎖榆鼠。
if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions ++ ;
return ;
}
//如果當(dāng)前線程是第一次進(jìn)入該monitor,設(shè)置_recursions為1亥鸠,_owner為當(dāng)前線程
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, "internal state error");
_recursions = 1 ;
// Commute owner from a thread-specific on-stack BasicLockObject address to
// a full-fledged "Thread *".
_owner = Self ;
OwnerIsThread = 1 ;
return ;
}
//省略部分代碼。
//通過(guò)自旋執(zhí)行ObjectMonitor::EnterI方法等待鎖的釋放
for (;;) {
jt->set_suspend_equivalent();
// cleared by handle_special_suspend_equivalent_condition()
// or java_suspend_self()
EnterI (THREAD) ;
if (!ExitSuspendEquivalent(jt)) break ;
//
// We have acquired the contended monitor, but while we were
// waiting another thread suspended us. We don't want to enter
// the monitor while suspended because that would surprise the
// thread that suspended us.
//
_recursions = 0 ;
_succ = NULL ;
exit (Self) ;
jt->java_suspend_self();
}
}
釋放鎖
void ATTR ObjectMonitor::exit(TRAPS) {
Thread * Self = THREAD ;
//如果當(dāng)前線程不是Monitor的所有者
if (THREAD != _owner) {
if (THREAD->is_lock_owned((address) _owner)) { //
// Transmute _owner from a BasicLock pointer to a Thread address.
// We don't need to hold _mutex for this transition.
// Non-null to Non-null is safe as long as all readers can
// tolerate either flavor.
assert (_recursions == 0, "invariant") ;
_owner = THREAD ;
_recursions = 0 ;
OwnerIsThread = 1 ;
} else {
// NOTE: we need to handle unbalanced monitor enter/exit
// in native code by throwing an exception.
// TODO: Throw an IllegalMonitorStateException ?
TEVENT (Exit - Throw IMSX) ;
assert(false, "Non-balanced monitor enter/exit!");
if (false) {
THROW(vmSymbols::java_lang_IllegalMonitorStateException());
}
return;
}
}
//如果_recursions次數(shù)不為0.自減
if (_recursions != 0) {
_recursions--; // this is simple recursive enter
TEVENT (Inflated exit - recursive) ;
return ;
}
省略部分代碼识啦,根據(jù)不同的策略(由QMode指定)负蚊,從cxq或EntryList中獲取頭節(jié)點(diǎn),通過(guò)ObjectMonitor::ExitEpilog方法喚醒該節(jié)點(diǎn)封裝的線程颓哮,喚醒操作最終由unpark完成家妆。由此看來(lái),monitor對(duì)象存在于每個(gè)Java對(duì)象的對(duì)象頭中(存儲(chǔ)的指針的指向)冕茅,synchronized鎖便是通過(guò)這種方式獲取鎖的伤极,也是為什么Java中任意對(duì)象可以作為鎖的原因蛹找,同時(shí)也是notify/notifyAll/wait等方法存在于頂級(jí)對(duì)象Object中的原因,在使用這3個(gè)方法時(shí)哨坪,必須處于synchronized代碼塊或者synchronized方法中庸疾,否則就會(huì)拋出IllegalMonitorStateException異常,這是因?yàn)檎{(diào)用這幾個(gè)方法前必須拿到當(dāng)前對(duì)象的監(jiān)視器monitor對(duì)象当编,也就是說(shuō)notify/notifyAll和wait方法依賴于monitor對(duì)象届慈,在前面的分析中,我們知道m(xù)onitor 存在于對(duì)象頭的Mark Word 中(存儲(chǔ)monitor引用指針)忿偷,而synchronized關(guān)鍵字可以獲取 monitor 金顿,這也就是為什么notify/notifyAll和wait方法必須在synchronized代碼塊或者synchronized方法調(diào)用的原因。下面我們將進(jìn)一步分析synchronized在字節(jié)碼層面的具體語(yǔ)義實(shí)現(xiàn)鲤桥。
synchronized底層原理
public class SynchronizedDemo {
//同步方法
public synchronized void doSth(){
System.out.println("Hello World");
}
//同步代碼塊
public void doSth1(){
synchronized (SynchronizedDemo.class){
System.out.println("Hello World");
}
}
}
被synchronized修飾的代碼塊及方法揍拆,在同一時(shí)間,只能被單個(gè)線程訪問(wèn)茶凳。
synchronized的實(shí)現(xiàn)原理
synchronized嫂拴,是Java中用于解決并發(fā)情況下數(shù)據(jù)同步訪問(wèn)的一個(gè)很重要的關(guān)鍵字。當(dāng)我們想要保證一個(gè)共享資源在同一時(shí)間只會(huì)被一個(gè)線程訪問(wèn)到時(shí)慧妄,我們可以在代碼中使用synchronized關(guān)鍵字對(duì)類或者對(duì)象加鎖顷牌。
我們對(duì)上面的代碼進(jìn)行反編譯,可以得到如下代碼:
public synchronized void doSth();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public void doSth1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #5 // class com/hollis/SynchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #3 // String Hello World
10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
通過(guò)反編譯后代碼可以看出:對(duì)于同步方法塞淹,JVM采用ACC_SYNCHRONIZED標(biāo)記符來(lái)實(shí)現(xiàn)同步窟蓝。 對(duì)于同步代碼塊。JVM采用monitorenter饱普、monitorexit兩個(gè)指令來(lái)實(shí)現(xiàn)同步运挫。
方法級(jí)的同步是隱式的。同步方法的常量池中會(huì)有一個(gè)ACC_SYNCHRONIZED標(biāo)志套耕。當(dāng)某個(gè)線程要訪問(wèn)某個(gè)方法的時(shí)候谁帕,會(huì)檢查是否有ACC_SYNCHRONIZED,如果有設(shè)置冯袍,則需要先獲得監(jiān)視器鎖匈挖,然后開(kāi)始執(zhí)行方法,方法執(zhí)行之后再釋放監(jiān)視器鎖康愤。這時(shí)如果其他線程來(lái)請(qǐng)求執(zhí)行方法儡循,會(huì)因?yàn)闊o(wú)法獲得監(jiān)視器鎖而被阻斷住。值得注意的是征冷,如果在方法執(zhí)行過(guò)程中择膝,發(fā)生了異常,并且方法內(nèi)部并沒(méi)有處理該異常检激,那么在異常被拋到方法外面之前監(jiān)視器鎖會(huì)被自動(dòng)釋放肴捉。
同步代碼塊使用monitorenter和monitorexit兩個(gè)指令實(shí)現(xiàn)腹侣。可以把執(zhí)行monitorenter指令理解為加鎖齿穗,執(zhí)行monitorexit理解為釋放鎖傲隶。 每個(gè)對(duì)象維護(hù)著一個(gè)記錄著被鎖次數(shù)的計(jì)數(shù)器。未被鎖定的對(duì)象的該計(jì)數(shù)器為0缤灵,當(dāng)一個(gè)線程獲得鎖(執(zhí)行monitorenter)后伦籍,該計(jì)數(shù)器自增變?yōu)?1 ,當(dāng)同一個(gè)線程再次獲得該對(duì)象的鎖的時(shí)候腮出,計(jì)數(shù)器再次自增帖鸦。當(dāng)同一個(gè)線程釋放鎖(執(zhí)行monitorexit指令)的時(shí)候,計(jì)數(shù)器再自減胚嘲。當(dāng)計(jì)數(shù)器為0的時(shí)候作儿。鎖將被釋放,其他線程便可以獲得鎖馋劈。
無(wú)論是ACC_SYNCHRONIZED還是monitorenter攻锰、monitorexit都是基于Monitor實(shí)現(xiàn)的,在Java虛擬機(jī)(HotSpot)中妓雾,Monitor是基于C++實(shí)現(xiàn)的娶吞,由ObjectMonitor實(shí)現(xiàn)。
ObjectMonitor類中提供了幾個(gè)方法械姻,如enter妒蛇、exit、wait楷拳、notify绣夺、notifyAll等。sychronized加鎖的時(shí)候欢揖,會(huì)調(diào)用objectMonitor的enter方法陶耍,解鎖的時(shí)候會(huì)調(diào)用exit方法。sychronized加鎖的時(shí)候她混,會(huì)調(diào)用objectMonitor的enter方法烈钞,解鎖的時(shí)候會(huì)調(diào)用exit方法。事實(shí)上坤按,只有在JDK1.6之前棵磷,synchronized的實(shí)現(xiàn)才會(huì)直接調(diào)用ObjectMonitor的enter和exit,這種鎖被稱之為重量級(jí)鎖晋涣。為什么說(shuō)這種方式操作鎖很重呢?
Java的線程是映射到操作系統(tǒng)原生線程之上的沉桌,如果要阻塞或喚醒一個(gè)線程就需要操作系統(tǒng)的幫忙谢鹊,這就要從用戶態(tài)轉(zhuǎn)換到核心態(tài)算吩,因此狀態(tài)轉(zhuǎn)換需要花費(fèi)很多的處理器時(shí)間,對(duì)于代碼簡(jiǎn)單的同步塊(如被synchronized修飾的get 或set方法)狀態(tài)轉(zhuǎn)換消耗的時(shí)間有可能比用戶代碼執(zhí)行的時(shí)間還要長(zhǎng)佃扼,所以說(shuō)synchronized是java語(yǔ)言中一個(gè)重量級(jí)的操縱偎巢。
所以,在JDK1.6中出現(xiàn)對(duì)鎖進(jìn)行了很多的優(yōu)化兼耀,進(jìn)而出現(xiàn)輕量級(jí)鎖压昼,偏向鎖,鎖消除瘤运,適應(yīng)性自旋鎖窍霞,鎖粗化(自旋鎖在1.4就有 只不過(guò)默認(rèn)的是關(guān)閉的,jdk1.6是默認(rèn)開(kāi)啟的)拯坟,這些操作都是為了在線程之間更高效的共享數(shù)據(jù)但金,解決競(jìng)爭(zhēng)問(wèn)題。
Java虛擬機(jī)對(duì)synchronized的優(yōu)化
鎖的狀態(tài)總共有四種郁季,無(wú)鎖狀態(tài)冷溃、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖梦裂。隨著鎖的競(jìng)爭(zhēng)似枕,鎖可以從偏向鎖升級(jí)到輕量級(jí)鎖,再升級(jí)的重量級(jí)鎖年柠,但是鎖的升級(jí)是單向的凿歼,也就是說(shuō)只能從低到高升級(jí),不會(huì)出現(xiàn)鎖的降級(jí)彪杉,關(guān)于重量級(jí)鎖毅往,前面我們已詳細(xì)分析過(guò),下面我們將介紹偏向鎖和輕量級(jí)鎖以及JVM的其他優(yōu)化手段派近。
自旋鎖與自適應(yīng)自旋
前面我們討論互斥同步的時(shí)候攀唯,提到了互斥同步對(duì)性能最大的影響是阻塞的實(shí)現(xiàn),掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成渴丸,這些操作給系統(tǒng)的并發(fā)性能帶來(lái)了很大的壓力侯嘀。同時(shí),虛擬機(jī)的開(kāi)發(fā)團(tuán)隊(duì)也注意到在許多應(yīng)用上谱轨,共享數(shù)據(jù)的鎖定狀態(tài)只會(huì)持續(xù)很短的一段時(shí)間戒幔,為了這段時(shí)間去掛起和恢復(fù)線程并不值得。如果物理機(jī)器有一個(gè)以上的處理器土童,能讓兩個(gè)或以上的線程同時(shí)并行執(zhí)行诗茎,我們就可以讓后面請(qǐng)求鎖的那個(gè)線程“稍等一 下”,但不放棄處理器的執(zhí)行時(shí)間献汗,看看持有鎖的線程是否很快就會(huì)釋放鎖敢订。為了讓線程等待王污,我們只需讓線程執(zhí)行一個(gè)忙循環(huán)(自旋),這項(xiàng)技術(shù)就是所謂的自旋鎖楚午。
自旋鎖在JDK 1.4.2中就已經(jīng)引入昭齐,只不過(guò)默認(rèn)是關(guān)閉的,可以使用 :XX:+UseSpinning 參數(shù)來(lái)開(kāi)啟矾柜,在JDK 1.6 中就已經(jīng)改為默認(rèn)開(kāi)啟了阱驾。自旋等待不能代替阻塞,且先不說(shuō)對(duì)處理器數(shù)量的要求怪蔑,自旋等待本身雖然避免了線程切換的開(kāi)銷里覆,但它是要占用處理器時(shí)間的,因此饮睬,如果鎖被占用的時(shí)間很短租谈,自旋等待的效果就會(huì)非常好,反之捆愁,如果鎖被占用的時(shí)間很長(zhǎng)割去,那么自旋的線程只會(huì)白白消耗處理器資源,而不會(huì)做任何有用的工作昼丑,反而會(huì)帶來(lái)性能上的浪費(fèi)呻逆。因此,自旋等待的時(shí)間必須要有一定的限度菩帝,如果自旋超過(guò)了限定的次數(shù)仍然沒(méi)有成功獲得鎖咖城,就應(yīng)當(dāng)使用傳統(tǒng)的方式去掛起線程了。自旋次數(shù)的默認(rèn)值是 10 次呼奢,用戶可以使用參數(shù)-XX:PreBlockSpin 來(lái)更改宜雀。
在JDK 1.6中引入了自適應(yīng)的自旋鎖。自適應(yīng)意味著自旋的時(shí)間不再固定了握础,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定辐董。如果在同一個(gè)鎖對(duì)象上,自旋等待剛剛成功獲得過(guò)鎖禀综,并且持有鎖的線程正在運(yùn)行中简烘,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也很有可能再次成功,進(jìn)而它將允許自旋等待持續(xù)相對(duì)更長(zhǎng)的時(shí)間定枷,比如100個(gè)循環(huán)孤澎。另外,如果對(duì)于某個(gè)鎖欠窒,自旋很少成功獲得過(guò)覆旭,那在以后要獲取這個(gè)鎖時(shí)將可能省略掉自旋過(guò)程,以避免浪費(fèi)處理部資源。有了自適應(yīng)自旋姐扮,隨著程序運(yùn)行和性能監(jiān)控信息的不斷完善絮供,虛擬機(jī)對(duì)程序鎖的狀況預(yù)測(cè)就會(huì)越來(lái)越準(zhǔn)確。虛擬機(jī)就會(huì)變得越來(lái)越 “聰明” 了茶敏。
鎖消除
鎖消除是指虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí),對(duì)一些代碼上要求同步缚俏,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除惊搏。鎖消除的主要判定依據(jù)來(lái)源于逃逸分析的數(shù)據(jù)支持,如果判斷在一段代碼中忧换,堆上的所有數(shù)據(jù)都不會(huì)逃逸出去從而被其他線程訪問(wèn)到恬惯,那就可以把它們當(dāng)做棧上數(shù)據(jù)對(duì)待,認(rèn)為它們是線程私有的亚茬,同步加鎖自然就無(wú)須進(jìn)行酪耳。
也許讀者會(huì)有疑問(wèn),變量是否逃逸刹缝,對(duì)于虛擬機(jī)來(lái)說(shuō)需要使用數(shù)據(jù)流分析來(lái)確定碗暗,但是程序員自己所該是很清楚的,怎么會(huì)在明知道不存在數(shù)據(jù)爭(zhēng)用的情況下要求同步呢梢夯?答案是有許多同步措施并不是程序員自己加入的言疗,同步的代碼在Java程序中的普遍程度也許超過(guò)了 大部分讀者的想象。我們來(lái)看看下面代碼中的例子颂砸,這段非常簡(jiǎn)單的代碼僅僅是輸出 3 個(gè)字符串相加的結(jié)果噪奄,無(wú)論是源碼字面上還是程序語(yǔ)義上都沒(méi)有同步。我們也知道人乓,由于 String 是一個(gè)不可變的類勤篮,對(duì)字符串的連接操作總是通過(guò)生成新的String 對(duì)象來(lái)進(jìn)行的,因此 Javac 編譯器會(huì)對(duì) String 連接做自動(dòng)優(yōu)化色罚。在 JDK 1.5 之前碰缔,會(huì)轉(zhuǎn)化為 StringBuffer 對(duì)象的連續(xù) append()操作,在JDK 1.5及以后的版本中保屯,會(huì)轉(zhuǎn)化為 StringBuilder 對(duì)象的連續(xù) append()操作手负,上述代碼可能會(huì)變成下面的樣子。
現(xiàn)在大家還認(rèn)為這段代碼沒(méi)有涉及同步嗎姑尺?每個(gè)StringBuffer.append() 方法中都有一個(gè)同步塊竟终,鎖就是 sb 對(duì)象。虛擬機(jī)觀察變量sb切蟋,很快就會(huì)發(fā)現(xiàn)它的動(dòng)態(tài)作用域被限制在 concatString() 方法內(nèi)部统捶。也就是說(shuō),sb 的所有引用永遠(yuǎn)不會(huì) “逃逸” 到 concatString()方法之外,其他線程無(wú)法訪問(wèn)到它喘鸟,因此匆绣,雖然這里有鎖,但是可以被安全地消除掉什黑,在即時(shí)編譯之后崎淳,這段代碼就會(huì)忽略掉所有的同步而直接執(zhí)行了。
鎖粗化
原則上愕把,我們?cè)诰帉懘a的時(shí)候拣凹,總是推薦將同步塊的作用范圍限制得盡量小,只在共享數(shù)據(jù)的實(shí)際作用域中才進(jìn)行同步恨豁,這樣是為了使得需要同步的操作數(shù)量盡可能變小嚣镜,如果存在鎖競(jìng)爭(zhēng),那等待鎖的線程也能盡快拿到鎖橘蜜。
大部分情況下菊匿,上面的原則都是正確的,但是如果一系列的連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖计福,甚至加鎖操作是出現(xiàn)在循環(huán)體中的跌捆,那即使沒(méi)有線程競(jìng)爭(zhēng),頻繁地進(jìn)行互斥同步操作也會(huì)導(dǎo)致不必要的性能損耗棒搜。
上述代碼中連續(xù)的append()方法就屬于這類情況疹蛉。如果虛擬機(jī)探測(cè)到有這樣一串零碎的操作都對(duì)同一個(gè)對(duì)象加鎖,將會(huì)把加鎖同步的范圍擴(kuò)展 (粗化)到整個(gè)操作序列的外部力麸,以上述代碼為例可款,就是擴(kuò)展到第一個(gè) append()操作之前直至最后一個(gè) append()操作之后,這樣只需要加鎖一次就可以了克蚂。
輕量級(jí)鎖
倘若偏向鎖失敗闺鲸,虛擬機(jī)并不會(huì)立即升級(jí)為重量級(jí)鎖,它還會(huì)嘗試使用一種稱為輕量級(jí)鎖的優(yōu)化手段(1.6之后加入的)埃叭,此時(shí)Mark Word 的結(jié)構(gòu)也變?yōu)檩p量級(jí)鎖的結(jié)構(gòu)摸恍。輕量級(jí)鎖能夠提升程序性能的依據(jù)是“對(duì)絕大部分的鎖,在整個(gè)同步周期內(nèi)都不存在競(jìng)爭(zhēng)”赤屋,注意這是經(jīng)驗(yàn)數(shù)據(jù)立镶。需要了解的是,輕量級(jí)鎖所適應(yīng)的場(chǎng)景是線程交替執(zhí)行同步塊的場(chǎng)合类早,如果存在同一時(shí)間訪問(wèn)同一鎖的場(chǎng)合媚媒,就會(huì)導(dǎo)致輕量級(jí)鎖膨脹為重量級(jí)鎖。
輕盤級(jí)鎖是JDK1.6之中加入的新型鎖機(jī)制涩僻,它名字中的 “輕量級(jí)” 是相對(duì)于使用操作系統(tǒng)互斥量來(lái)實(shí)現(xiàn)的傳統(tǒng)鎖而言的缭召,因此傳統(tǒng)的鎖機(jī)制就稱為 “重量級(jí)” 鎖栈顷。首先需要強(qiáng)調(diào)一點(diǎn)的是,輕量級(jí)鎖并不是用來(lái)代替重量級(jí)鎖的嵌巷,它的本意是在沒(méi)有多線程競(jìng)爭(zhēng)的前提下萄凤,減少傳統(tǒng)的重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。
在代碼進(jìn)入同步塊的時(shí)候搪哪,如果此同步對(duì)象沒(méi)有被鎖定(鎖標(biāo)志位為“01”狀態(tài))靡努,虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的 Mark Word 的拷貝 (官方把這份拷貝加了一個(gè) Displaced 前轍噩死,即Displaced Mark Word )颤难,這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如圖所示。
然后已维,虛擬機(jī)將使用CAS操作嘗試將對(duì)象的 Mark Word 更新為指向 Lock Record 的指針。如果這個(gè)更新動(dòng)作成功了已日,那么這個(gè)線程就擁有了該對(duì)象的鎖垛耳,并且對(duì)象 Mark Word 的鎖標(biāo)志位(Mark Word 的最后 2bit )將轉(zhuǎn)變?yōu)?“00”,即表示此對(duì)象處于輕量級(jí)鎖定狀態(tài)飘千,這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如圖所示堂鲜。如果這個(gè)更新操作失敗了,虛擬機(jī)首先會(huì)檢查對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀护奈,如果是說(shuō)明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖缔莲,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行,否則說(shuō)明這個(gè)鎖對(duì)象已經(jīng)被其他線程搶占了霉旗。如果有兩條以上的線程爭(zhēng)用同一個(gè)鎖痴奏,那輕量級(jí)鎖就不再有效,要膨脹為重量級(jí)鎖厌秒,鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”读拆,Mark Word 中存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針,后而等待鎖的線程也要進(jìn)入阻塞狀態(tài)鸵闪。
上而描述的是輕量級(jí)鎖的加鎖過(guò)程檐晕,它的解鎖過(guò)程也是通過(guò)CAS操作來(lái)進(jìn)行的,如果對(duì)象的 Mark Word 仍然指向著線程的鎖記錄蚌讼,那就用 CAS 操作把對(duì)象當(dāng)前的 Mark Word 和 線程中復(fù)制的 Displaced Mark Word 替換回來(lái)辟灰,如果替換成功,越個(gè)同步過(guò)程就完成了篡石。如果替換失敗芥喇,說(shuō)明有其他線程嘗試過(guò)獲取該鎖,那就要在釋放鎖的同時(shí)夏志,喚醒被掛起的線程乃坤。
輕量級(jí)鎖能提升程序同步性能的依據(jù)是“對(duì)于絕大部分的鎖苛让,在整個(gè)同步周期內(nèi)都是不存在競(jìng)爭(zhēng)的”,這是一個(gè)經(jīng)驗(yàn)數(shù)據(jù)湿诊。如果沒(méi)有競(jìng)爭(zhēng)狱杰,輕量級(jí)鎖使用 CAS 操作避免了使用互斥量的開(kāi)銷,但如果存在鎖競(jìng)爭(zhēng)厅须,除了互斥量的開(kāi)銷外仿畸,還額外發(fā)生了 CAS 操作,因此在有競(jìng)爭(zhēng)的情況下朗和,輕量級(jí)鎖會(huì)比傳統(tǒng)的重量級(jí)鎖更慢错沽。
偏向鎖
偏向鎖是Java 6之后加入的新鎖,它是一種針對(duì)加鎖操作的優(yōu)化手段眶拉,經(jīng)過(guò)研究發(fā)現(xiàn)千埃,在大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng)忆植,而且總是由同一線程多次獲得放可,因此為了減少同一線程獲取鎖(會(huì)涉及到一些CAS操作,耗時(shí))的代價(jià)而引入偏向鎖。偏向鎖的核心思想是朝刊,如果一個(gè)線程獲得了鎖耀里,那么鎖就進(jìn)入偏向模式,此時(shí)Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu)拾氓,當(dāng)這個(gè)線程再次請(qǐng)求鎖時(shí)冯挎,無(wú)需再做任何同步操作,即獲取鎖的過(guò)程咙鞍,這樣就省去了大量有關(guān)鎖申請(qǐng)的操作房官,從而也就提供程序的性能。所以奶陈,對(duì)于沒(méi)有鎖競(jìng)爭(zhēng)的場(chǎng)合易阳,偏向鎖有很好的優(yōu)化效果,畢竟極有可能連續(xù)多次是同一個(gè)線程申請(qǐng)相同的鎖吃粒。但是對(duì)于鎖競(jìng)爭(zhēng)比較激烈的場(chǎng)合潦俺,偏向鎖就失效了,因?yàn)檫@樣場(chǎng)合極有可能每次申請(qǐng)鎖的線程都是不相同的徐勃,因此這種場(chǎng)合下不應(yīng)該使用偏向鎖事示,否則會(huì)得不償失,需要注意的是僻肖,偏向鎖失敗后肖爵,并不會(huì)立即膨脹為重量級(jí)鎖,而是先升級(jí)為輕量級(jí)鎖臀脏。
偏向鎖的目的是消除數(shù)據(jù)在無(wú)競(jìng)爭(zhēng)情況下的同步原語(yǔ)劝堪,進(jìn)一步提高程序的運(yùn)行性能冀自。如果說(shuō)輕量級(jí)鎖是在無(wú)競(jìng)爭(zhēng)的情況下使用CAS操作去消除同步使用的互斥量,那偏向鎖就是在無(wú)競(jìng)爭(zhēng)的情況下把整個(gè)同步都消除掉秒啦,連 CAS 操作都不做了熬粗。
偏向鎖的“偏”,就是偏心的 “偏”余境、偏袒的 “偏”驻呐,它的意思是這個(gè)鎖會(huì)偏向于第一個(gè)獲得它的線程,如果在接下來(lái)的執(zhí)行過(guò)程中芳来,該鎖沒(méi)有被其他的線程獲取含末,則持有偏向鎖的線程將永遠(yuǎn)不需要再進(jìn)行同步。
如果讀者讀懂了前面輕量級(jí)鎖中關(guān)于對(duì)象頭Mark Word與線程之間的操作過(guò)程即舌,那偏向鎖的原理理解起來(lái)就會(huì)很簡(jiǎn)單佣盒。假設(shè)當(dāng)前虛擬機(jī)啟用了偏向(啟用參數(shù)-XX:+UseBiasedLocking,這是 JDK 1.6 的默認(rèn)值)顽聂,那么沼撕,當(dāng)鎖對(duì)象第一次被線程獲取的時(shí)候,虛擬機(jī)將會(huì)把對(duì)象頭中的標(biāo)志位設(shè)為 “01 ”芜飘,即偏向模式。同時(shí)使用 CAS 操作把獲取到這個(gè)鎖的線程的 ID記錄在對(duì)象的 Mark Word 之中磨总,如果 CAS 操作成功嗦明,持有偏向鎖的錢程以后每次進(jìn)入這個(gè)鎖相關(guān)的同步塊時(shí),虛擬機(jī)都可以不再進(jìn)行任何同步操作(例如 Locking 蚪燕、Unlocking 及對(duì) Mark Word的Update 等)娶牌。
當(dāng)有另外一個(gè)線程去嘗試獲取這個(gè)鎖時(shí),偏向模式就宣告結(jié)束馆纳。根據(jù)鎖對(duì)象目前是否處于被鎖定的狀態(tài)诗良,撤銷偏向(Revoke Bias)后恢復(fù)到未鎖定(標(biāo)志位為“01”) 或輕量級(jí)鎖定(標(biāo)志位為 “00”)的狀態(tài),后續(xù)的同步操作就如上面介紹的輕量級(jí)鎖那樣執(zhí)行鲁驶。偏向鎖鉴裹、 輕量級(jí)鎖的狀態(tài)轉(zhuǎn)化及對(duì)象 Mark Word 的關(guān)系如圖所示。同步但無(wú)競(jìng)爭(zhēng)的程序性能钥弯。它同樣是一個(gè)帶有效益權(quán)衡(Trade Off)性質(zhì)的優(yōu)化径荔,也就是說(shuō),它并不一定總是對(duì)程序運(yùn)行有利脆霎,如果程序中大多數(shù)的鎖總是被多個(gè)不同的線程訪問(wèn)总处,那偏向模式就是多余的。在具體問(wèn)題具體分析的前提下睛蛛,有時(shí)候使用參數(shù)XX:-UseBiasedLocking來(lái)禁止偏向鎖優(yōu)化反而可以提升性能鹦马。