理解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ù)沒有關(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í)鎖也就是通常說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)。 先來舉個(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)來選擇排隊(duì)的客戶進(jìn)入房間。如果,因?yàn)槟承┰颍摽蛻魰簳r(shí)因?yàn)槠渌虑闊o法脫身(線程被掛起),那么他將被送到另外一間專門用來等待的房間(Wait Set),這個(gè)房間的可以可以在稍后再次進(jìn)入那件特殊的房間。如上面所說,這個(gè)建筑屋中一共有三個(gè)場(chǎng)所。
總之,監(jiān)視器是一個(gè)用來監(jiān)視這些線程進(jìn)入特殊的房間的。他的義務(wù)是保證(同一時(shí)間)只有一個(gè)線程可以訪問被保護(hù)的數(shù)據(jù)和代碼。
Monitor其實(shí)是一種同步工具,也可以說是一種同步機(jī)制,它通常被描述為一個(gè)對(duì)象,主要特點(diǎn)是:
- 對(duì)象的所有方法都被“互斥”的執(zhí)行。好比一個(gè)Monitor只有一個(gè)運(yùn)行“許可”,任一個(gè)線程進(jìn)入任何一個(gè)方法都需要獲得這個(gè)“許可”,離開時(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:用來記錄該線程獲取鎖的次數(shù)
當(dāng)多個(gè)線程同時(shí)訪問一段同步代碼時(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 ;
//通過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)前線程一樣,說明當(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 ;
}
//省略部分代碼。
//通過自旋執(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),通過ObjectMonitor::ExitEpilog方法喚醒該節(jié)點(diǎn)封裝的線程,喚醒操作最終由unpark完成。由此看來,monitor對(duì)象存在于每個(gè)Java對(duì)象的對(duì)象頭中(存儲(chǔ)的指針的指向),synchronized鎖便是通過這種方式獲取鎖的,也是為什么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ì)象,也就是說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é)碼層面的具體語義實(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è)線程訪問。
synchronized的實(shí)現(xiàn)原理
synchronized,是Java中用于解決并發(fā)情況下數(shù)據(jù)同步訪問的一個(gè)很重要的關(guān)鍵字。當(dāng)我們想要保證一個(gè)共享資源在同一時(shí)間只會(huì)被一個(gè)線程訪問到時(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
通過反編譯后代碼可以看出:對(duì)于同步方法,JVM采用ACC_SYNCHRONIZED標(biāo)記符來實(shí)現(xiàn)同步。 對(duì)于同步代碼塊。JVM采用monitorenter、monitorexit兩個(gè)指令來實(shí)現(xiàn)同步。
方法級(jí)的同步是隱式的。同步方法的常量池中會(huì)有一個(gè)ACC_SYNCHRONIZED標(biāo)志。當(dāng)某個(gè)線程要訪問某個(gè)方法的時(shí)候,會(huì)檢查是否有ACC_SYNCHRONIZED,如果有設(shè)置,則需要先獲得監(jiān)視器鎖,然后開始執(zhí)行方法,方法執(zhí)行之后再釋放監(jiān)視器鎖。這時(shí)如果其他線程來請(qǐng)求執(zhí)行方法,會(huì)因?yàn)闊o法獲得監(jiān)視器鎖而被阻斷住。值得注意的是,如果在方法執(zhí)行過程中,發(fā)生了異常,并且方法內(nè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í)候。鎖將被釋放,其他線程便可以獲得鎖。
無論是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í)鎖。為什么說這種方式操作鎖很重呢?
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),所以說synchronized是java語言中一個(gè)重量級(jí)的操縱。
所以,在JDK1.6中出現(xiàn)對(duì)鎖進(jìn)行了很多的優(yōu)化,進(jìn)而出現(xiàn)輕量級(jí)鎖,偏向鎖,鎖消除,適應(yīng)性自旋鎖,鎖粗化(自旋鎖在1.4就有 只不過默認(rèn)的是關(guān)閉的,jdk1.6是默認(rèn)開啟的),這些操作都是為了在線程之間更高效的共享數(shù)據(jù),解決競(jìng)爭(zhēng)問題。
Java虛擬機(jī)對(duì)synchronized的優(yōu)化
鎖的狀態(tài)總共有四種,無鎖狀態(tài)、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖。隨著鎖的競(jìng)爭(zhēng),鎖可以從偏向鎖升級(jí)到輕量級(jí)鎖,再升級(jí)的重量級(jí)鎖,但是鎖的升級(jí)是單向的,也就是說只能從低到高升級(jí),不會(huì)出現(xiàn)鎖的降級(jí),關(guān)于重量級(jí)鎖,前面我們已詳細(xì)分析過,下面我們將介紹偏向鎖和輕量級(jí)鎖以及JVM的其他優(yōu)化手段。
自旋鎖與自適應(yīng)自旋
前面我們討論互斥同步的時(shí)候,提到了互斥同步對(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è)或以上的線程同時(shí)并行執(zhí)行,我們就可以讓后面請(qǐng)求鎖的那個(gè)線程“稍等一 下”,但不放棄處理器的執(zhí)行時(shí)間,看看持有鎖的線程是否很快就會(huì)釋放鎖。為了讓線程等待,我們只需讓線程執(zhí)行一個(gè)忙循環(huán)(自旋),這項(xiàng)技術(shù)就是所謂的自旋鎖。
自旋鎖在JDK 1.4.2中就已經(jīng)引入,只不過默認(rèn)是關(guān)閉的,可以使用 :XX:+UseSpinning 參數(shù)來開啟,在JDK 1.6 中就已經(jīng)改為默認(rèn)開啟了。自旋等待不能代替阻塞,且先不說對(duì)處理器數(shù)量的要求,自旋等待本身雖然避免了線程切換的開銷,但它是要占用處理器時(shí)間的,因此,如果鎖被占用的時(shí)間很短,自旋等待的效果就會(huì)非常好,反之,如果鎖被占用的時(shí)間很長(zhǎng),那么自旋的線程只會(huì)白白消耗處理器資源,而不會(huì)做任何有用的工作,反而會(huì)帶來性能上的浪費(fèi)。因此,自旋等待的時(shí)間必須要有一定的限度,如果自旋超過了限定的次數(shù)仍然沒有成功獲得鎖,就應(yīng)當(dāng)使用傳統(tǒng)的方式去掛起線程了。自旋次數(shù)的默認(rèn)值是 10 次,用戶可以使用參數(shù)-XX:PreBlockSpin 來更改。
在JDK 1.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ī)就會(huì)變得越來越 “聰明” 了。
鎖消除
鎖消除是指虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí),對(duì)一些代碼上要求同步,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除。鎖消除的主要判定依據(jù)來源于逃逸分析的數(shù)據(jù)支持,如果判斷在一段代碼中,堆上的所有數(shù)據(jù)都不會(huì)逃逸出去從而被其他線程訪問到,那就可以把它們當(dāng)做棧上數(shù)據(jù)對(duì)待,認(rèn)為它們是線程私有的,同步加鎖自然就無須進(jìn)行。
也許讀者會(huì)有疑問,變量是否逃逸,對(duì)于虛擬機(jī)來說需要使用數(shù)據(jù)流分析來確定,但是程序員自己所該是很清楚的,怎么會(huì)在明知道不存在數(shù)據(jù)爭(zhēng)用的情況下要求同步呢?答案是有許多同步措施并不是程序員自己加入的,同步的代碼在Java程序中的普遍程度也許超過了 大部分讀者的想象。我們來看看下面代碼中的例子,這段非常簡(jiǎn)單的代碼僅僅是輸出 3 個(gè)字符串相加的結(jié)果,無論是源碼字面上還是程序語義上都沒有同步。我們也知道,由于 String 是一個(gè)不可變的類,對(duì)字符串的連接操作總是通過生成新的String 對(duì)象來進(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)為這段代碼沒有涉及同步嗎?每個(gè)StringBuffer.append() 方法中都有一個(gè)同步塊,鎖就是 sb 對(duì)象。虛擬機(jī)觀察變量sb,很快就會(huì)發(fā)現(xiàn)它的動(dòng)態(tài)作用域被限制在 concatString() 方法內(nèi)部。也就是說,sb 的所有引用永遠(yuǎn)不會(huì) “逃逸” 到 concatString()方法之外,其他線程無法訪問到它,因此,雖然這里有鎖,但是可以被安全地消除掉,在即時(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)體中的,那即使沒有線程競(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í)間訪問同一鎖的場(chǎng)合,就會(huì)導(dǎo)致輕量級(jí)鎖膨脹為重量級(jí)鎖。
輕盤級(jí)鎖是JDK1.6之中加入的新型鎖機(jī)制,它名字中的 “輕量級(jí)” 是相對(duì)于使用操作系統(tǒng)互斥量來實(shí)現(xiàn)的傳統(tǒng)鎖而言的,因此傳統(tǒng)的鎖機(jī)制就稱為 “重量級(jí)” 鎖。首先需要強(qiáng)調(diào)一點(diǎn)的是,輕量級(jí)鎖并不是用來代替重量級(jí)鎖的,它的本意是在沒有多線程競(jìng)爭(zhēng)的前提下,減少傳統(tǒng)的重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。
在代碼進(jìn)入同步塊的時(shí)候,如果此同步對(duì)象沒有被鎖定(鎖標(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)前線程的棧幀,如果是說明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行,否則說明這個(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í)鎖的加鎖過程,它的解鎖過程也是通過CAS操作來進(jìn)行的,如果對(duì)象的 Mark Word 仍然指向著線程的鎖記錄,那就用 CAS 操作把對(duì)象當(dāng)前的 Mark Word 和 線程中復(fù)制的 Displaced Mark Word 替換回來,如果替換成功,越個(gè)同步過程就完成了。如果替換失敗,說明有其他線程嘗試過獲取該鎖,那就要在釋放鎖的同時(shí),喚醒被掛起的線程。
輕量級(jí)鎖能提升程序同步性能的依據(jù)是“對(duì)于絕大部分的鎖,在整個(gè)同步周期內(nèi)都是不存在競(jìng)爭(zhēng)的”,這是一個(gè)經(jīng)驗(yàn)數(shù)據(jù)。如果沒有競(jìng)爭(zhēng),輕量級(jí)鎖使用 CAS 操作避免了使用互斥量的開銷,但如果存在鎖競(jìng)爭(zhēng),除了互斥量的開銷外,還額外發(fā)生了 CAS 操作,因此在有競(jìng)爭(zhēng)的情況下,輕量級(jí)鎖會(huì)比傳統(tǒng)的重量級(jí)鎖更慢。
偏向鎖
偏向鎖是Java 6之后加入的新鎖,它是一種針對(duì)加鎖操作的優(yōu)化手段,經(jīng)過研究發(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í),無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關(guān)鎖申請(qǐng)的操作,從而也就提供程序的性能。所以,對(duì)于沒有鎖競(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ù)在無競(jìng)爭(zhēng)情況下的同步原語,進(jìn)一步提高程序的運(yùn)行性能。如果說輕量級(jí)鎖是在無競(jìng)爭(zhēng)的情況下使用CAS操作去消除同步使用的互斥量,那偏向鎖就是在無競(jìng)爭(zhēng)的情況下把整個(gè)同步都消除掉,連 CAS 操作都不做了。
偏向鎖的“偏”,就是偏心的 “偏”、偏袒的 “偏”,它的意思是這個(gè)鎖會(huì)偏向于第一個(gè)獲得它的線程,如果在接下來的執(zhí)行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠(yuǎn)不需要再進(jìn)行同步。
如果讀者讀懂了前面輕量級(jí)鎖中關(guān)于對(duì)象頭Mark Word與線程之間的操作過程,那偏向鎖的原理理解起來就會(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)系如圖所示。同步但無競(jìng)爭(zhēng)的程序性能。它同樣是一個(gè)帶有效益權(quán)衡(Trade Off)性質(zhì)的優(yōu)化,也就是說,它并不一定總是對(duì)程序運(yùn)行有利,如果程序中大多數(shù)的鎖總是被多個(gè)不同的線程訪問,那偏向模式就是多余的。在具體問題具體分析的前提下,有時(shí)候使用參數(shù)XX:-UseBiasedLocking來禁止偏向鎖優(yōu)化反而可以提升性能。