深入解析 Java 中的 synchronized 關(guān)鍵字
在多線程編程中,確保數(shù)據(jù)的一致性和線程安全是至關(guān)重要的問(wèn)題。Java 語(yǔ)言提供了多種機(jī)制來(lái)實(shí)現(xiàn)這一目標(biāo),其中 synchronized 關(guān)鍵字是最常用且最基礎(chǔ)的一種同步工具。本文將通過(guò)具體的使用示例和詳細(xì)的底層原理分析,幫助讀者全面理解 synchronized 的工作方式及其在實(shí)際開(kāi)發(fā)中的應(yīng)用。
一、synchronized是什么?有什么用?
synchronized是在多線程場(chǎng)景經(jīng)常用到的關(guān)鍵字,通過(guò)synchronized將共享資源設(shè)置為臨界資源,確保并發(fā)場(chǎng)景下共享資源操作的正確性:
二、synchronized基礎(chǔ)使用示例
1.synchronized作用于靜態(tài)方法
synchronized作用于靜態(tài)方法上,鎖的對(duì)象為Class,這就意味著方法的調(diào)用者無(wú)論是Class還是實(shí)例對(duì)象都可以保持互斥,所以下面這段代碼的結(jié)果為200:
public
class
SynchronizedDemo
{
private
static Logger logger = LoggerFactory.getLogger(SynchronizedDemo.class);
private
static
int count = 0;
/**
* synchronized作用域靜態(tài)類上
*/
public
synchronized
static
void
method()
{
count++;
}
@Test
public
void
test()
{
IntStream.rangeClosed(1,1_0000)
.parallel()
.forEach(i->SynchronizedDemo.method());
IntStream.rangeClosed(1,1_0000)
.parallel()
.forEach(i->new SynchronizedDemo().method());
logger.info("count:{}",count);
}
}
輸出結(jié)果:
22:59:44.647 [main] INFO com.sharkChili.webTemplate.SynchronizedDemo - count:20000
2.synchronized作用于方法
作用于方法上,則鎖住的對(duì)象是調(diào)用的示例對(duì)象,如果我們使用下面這段寫法,最終的結(jié)果卻不是10000。
private
static Logger logger = LoggerFactory.getLogger(SynchronizedDemo.class);
private
static
int count = 0;
/**
* synchronized作用域?qū)嵗椒ㄉ? */
public
synchronized
void
method()
{
count++;
}
@Test
public
void
test()
{
IntStream.rangeClosed(1,1_0000)
.parallel()
.forEach(i->new SynchronizedDemo().method());
logger.info("count:{}",count);
}
}
輸出結(jié)果:
2023-03-16
21:03:44,300 INFO SynchronizedDemo:30 - count:8786
因?yàn)閟ynchronized 作用于實(shí)例方法,會(huì)導(dǎo)致每個(gè)線程獲得的鎖都是各自使用的實(shí)例對(duì)象,而++操作又非原子操作,導(dǎo)致互斥失敗進(jìn)而導(dǎo)致數(shù)據(jù)錯(cuò)誤。 什么是原子操作呢?通俗的來(lái)說(shuō)就是一件事情只要一條指令就能完成,而count++在底層匯編指令如下所示,可以看到++操作實(shí)際上是需要3個(gè)步驟完成的:
- 從內(nèi)存將count讀取到寄存器
- count自增
- 寫回內(nèi)存
__asm
{
moveax, dword ptr[i]
inc eax
mov dwordptr[i], eax
}
正是由于鎖互斥的失敗,導(dǎo)致兩個(gè)線程同時(shí)到臨界區(qū)域加載資源,獲得的count都是0,經(jīng)過(guò)自增后都是1,導(dǎo)致數(shù)據(jù)少了1。
所以正確的使用方式是多個(gè)線程使用同一個(gè)對(duì)象調(diào)用該方法:
SynchronizedDemo demo = new SynchronizedDemo();
IntStream.rangeClosed(1,1_0000)
.parallel()
.forEach(i->demo.method());
logger.info("count:{}",count);
這樣一來(lái)輸出的結(jié)果就正常了。
2023-03-16
23:08:23,656 INFO SynchronizedDemo:31 - count:10000
3.synchronized作用于代碼塊
作用于代碼塊上的synchronized鎖住的就是括號(hào)內(nèi)的對(duì)象實(shí)例,以下面這段代碼為例,鎖的就是當(dāng)前調(diào)用者。
public
void
method()
{
synchronized (this) {
count++;
}
}
所以我們的使用的方式還是和作用與實(shí)例方法上一樣。
@Test
public
void
test()
{
SynchronizedDemo demo = new SynchronizedDemo();
IntStream.rangeClosed(1, 1_0000)
.parallel()
.forEach(i -> demo.method());
logger.info("count:{}", count);
}
輸出結(jié)果也是10000:
2023-03-16
23:11:08,496 INFO SynchronizedDemo:33 - count:10000
三、深入理解synchronized關(guān)鍵字
1.synchronized工作的本質(zhì)
我們給出一段synchronized作用于代碼塊上的方法:
public
class
SynchronizedDemo
{
private
static
int count = 0;
/**
* synchronized作用域?qū)嵗椒▋?nèi)
*/
public
void
method()
{
synchronized (this) {
count++;
}
}
public
static
void
main(String[] args)
{
SynchronizedDemo demo = new SynchronizedDemo();
IntStream.rangeClosed(1, 1_0000)
.parallel()
.forEach(i -> demo.method());
System.out.println("count:" + count);
}
}
先使用javac指令生成class文件:
javac SynchronizedDemo.java
然后再使用反編譯指令javap獲取反編譯后的代碼信息:
javap -c -s -v SynchronizedDemo.class
最終我們可以看到method方法的字節(jié)碼指令,可以看到關(guān)鍵字synchronized 的鎖是通過(guò)monitorenter和monitorexit來(lái)確保線程間的同步。
public
void
method();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2
// Field count:I
7: iconst_1
8: iadd
9: putstatic #2
// Field count:I
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
我們?cè)賹ynchronized 關(guān)鍵字改到方法上再次進(jìn)行編譯和反編譯:
public
synchronized
void
method()
{
count++;
}
可以看到synchronized 實(shí)現(xiàn)鎖的方式編程了通過(guò)ACC_SYNCHRONIZED關(guān)鍵字來(lái)標(biāo)明該方法是一個(gè)同步方法:
public
synchronized
void
method();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2
// Field count:I
3: iconst_1
4: iadd
5: putstatic #2
// Field count:I
8: return
LineNumberTable:
line 17: 0
line 19: 8
了解了不同synchronized在不同位置使用的指令之后,我們?cè)賮?lái)聊聊這些指令如何實(shí)現(xiàn)"鎖"的。
因?yàn)镴DK1.6之后提出鎖升級(jí)的機(jī)制,涉及不同層面的鎖的過(guò)程,這里我們直接以默認(rèn)情況下最高級(jí)別的重量級(jí)鎖為例展開(kāi)探究。
每個(gè)線程使用的實(shí)例對(duì)象都有一個(gè)對(duì)象頭,每個(gè)對(duì)象頭中都有一個(gè)Mark Word,當(dāng)我們使用synchronized 關(guān)鍵字時(shí),這個(gè)Mark Word就會(huì)指向一個(gè)monitor。 這個(gè)monitor鎖就是一種同步工具,是實(shí)現(xiàn)線程操作臨界資源互斥的關(guān)鍵所在,在Java HotSpot虛擬機(jī)中,monitor就是通過(guò)ObjectMonitor實(shí)現(xiàn)的。
其代碼如下,我們可以看到_EntryList、_WaitSet 、_owner三個(gè)關(guān)鍵屬性:
ObjectMonitor() {
_header = NULL;
_count = 0; // 記錄線程獲取鎖的次數(shù)
_waiters = 0,
_recursions = 0; //鎖的重入次數(shù)
_object = NULL;
_owner = NULL; // 指向持有ObjectMonitor對(duì)象的線程
_WaitSet = NULL; // 處于wait狀態(tài)的線程,會(huì)被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 處于等待鎖block狀態(tài)的線程,會(huì)被加入到該列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
我們假設(shè)自己現(xiàn)在就是一個(gè)需要獲取鎖的線程,要獲取ObjectMonitor鎖,所以我們經(jīng)過(guò)了下面幾個(gè)步驟:
- 進(jìn)入_EntryList。
- 嘗試取鎖,發(fā)現(xiàn)_owner區(qū)被其他線程持有,于是進(jìn)入_WaitSet 。
- 其他線程用完鎖,將count--變?yōu)?,釋放鎖,_owner被清空。
- 我們有機(jī)會(huì)獲取_owner,嘗試爭(zhēng)搶,成功獲取鎖,_owner指向我們這個(gè)線程,將count++。
- 我們操作到一半發(fā)現(xiàn)CPU時(shí)間片用完了,調(diào)用wait方法,線程再次進(jìn)入_WaitSet ,count--變?yōu)?,_owner被清空。
- 我們又有機(jī)會(huì)獲取_owner,嘗試爭(zhēng)搶,成功獲取鎖,將count++。
- 這一次,我們用完臨界資源,準(zhǔn)備釋放鎖,count--變?yōu)?,_owner清空,其他線程繼續(xù)進(jìn)行monitor爭(zhēng)搶。
2.synchronized如何保證可見(jiàn)性、有序性、可重入性
我們先來(lái)說(shuō)說(shuō)可見(jiàn)性,每個(gè)線程使用synchronized獲得鎖操作臨界資源時(shí),首先需要獲取臨界資源的值,為了保證臨界資源的值是最新的,JMM模型規(guī)定線程必須將本地工作內(nèi)存清空,到共享內(nèi)存中加載最新的進(jìn)行操作。 當(dāng)前線程上鎖后,其他線程是無(wú)法操作這個(gè)臨界資源的。當(dāng)前線程操作完臨界資源之后,會(huì)立刻將值寫回主存中,正是由于每個(gè)線程操作期間其他線程無(wú)法干擾,且臨界資源數(shù)據(jù)實(shí)時(shí)同步,所以synchronized關(guān)鍵字保證了臨界資源數(shù)據(jù)的可見(jiàn)性。
再來(lái)說(shuō)說(shuō)有序性,synchronized同步的代碼塊具備排他性,這就意味著同一個(gè)時(shí)刻只有一個(gè)線程可以獲得鎖,synchronized代碼塊的內(nèi)部資源是單線程執(zhí)行的。同時(shí)synchronized也遵守as-if-serial原則,可以當(dāng)線程線程修改最終結(jié)果是可以保證最終有序性,注意這里筆者說(shuō)的保證最終結(jié)果的有序性。
具體例子,某段線程得到鎖Test.class之后,執(zhí)行臨界代碼邏輯,可能會(huì)先執(zhí)行變量b初始化的邏輯,在執(zhí)行a變量初始化的邏輯,但是最終結(jié)果都會(huì)執(zhí)行a+b的邏輯。這也就我們的說(shuō)的保證最終結(jié)果的有序,而不保證執(zhí)行過(guò)程中的指令有序。
synchronized (Test.class) {
int a=1;
int b=2;
int c=a+b;
最后就是有序性了,Java允許同一個(gè)線程獲取同一把鎖兩次,即可重入性,原因我們上文將synchronized相關(guān)的ObjectMonitor鎖已經(jīng)提到了,ObjectMonitor有一個(gè)count變量就是用于記錄當(dāng)前線程獲取這把鎖的次數(shù)。 就像下面這段代碼,例如我們的線程T1,兩次執(zhí)行synchronized 獲取鎖Test.class兩次,count就自增兩次變?yōu)?。 退出synchronized關(guān)鍵字對(duì)應(yīng)的代碼塊,count就自減,變?yōu)?時(shí)就代表釋放了這把鎖,其他線程就可以爭(zhēng)搶這把鎖了。所以當(dāng)我們的線程退出下面的兩個(gè)synchronized 代碼塊時(shí),其他線程就可以爭(zhēng)搶Test.class這把鎖了。
public
void
add2()
{
synchronized (Test.class) {
synchronized (Test.class){
list.add(1);
}
}
}
四、詳解synchronized鎖粗化和鎖消除
1.鎖粗化
當(dāng)jvm發(fā)現(xiàn)操作的方法連續(xù)對(duì)同一把鎖進(jìn)行加鎖、解鎖操作,就會(huì)對(duì)鎖進(jìn)行粗化,所有操作都在同一把鎖中完成:
如下代碼所示,該方法內(nèi)部連續(xù)3次上同一把鎖,存在頻繁上鎖執(zhí)行monitorenter和monitorexit的開(kāi)銷:
private
static
void
func1()
{
synchronized (lock) {
System.out.println("lock first");
}
synchronized (lock) {
System.out.println("lock second");
}
synchronized (lock) {
System.out.println("lock third");
}
}
這一點(diǎn)我們通過(guò)jclasslib查看字節(jié)碼即可知曉這一點(diǎn):
對(duì)此JIT編譯器一旦感知到這種一個(gè)操作頻繁加解同一把鎖的情況,便會(huì)將鎖進(jìn)行粗化,最終的代碼效果大概是這樣:
private
static
void
func1()
{
synchronized (lock) {
System.out.println("lock first");
System.out.println("lock second");
System.out.println("lock third");
}
}
2.鎖消除
虛擬機(jī)在JIT即時(shí)編譯運(yùn)行時(shí),對(duì)一些代碼上要求同步,但是檢測(cè)到不存在共享數(shù)據(jù)的鎖的進(jìn)行消除。
下面這段代碼涉及字符串拼接操作,所以jvm會(huì)將其優(yōu)化為StringBuffer或者StringBuilder,至于選哪個(gè),這就需要進(jìn)行逃逸分析了。逃逸分析通俗來(lái)說(shuō)就是判斷當(dāng)前操作的對(duì)象是否會(huì)逃逸出去被其他線程訪問(wèn)到。
public String appendStr(String str1, String str2, String str3)
{
String result = str1 + str2 + str3;
return result;
}
例如我們上面的result ,是局部變量,沒(méi)有發(fā)生逃逸,所以完全可以當(dāng)作棧上數(shù)據(jù)來(lái)對(duì)待,是線程安全的,所以jvm進(jìn)行鎖消除,使用StringBuilder而不是Stringbuffer完成字符串拼接:
這一點(diǎn)我們可以在字節(jié)碼文件中得到印證
五、synchronized的鎖升級(jí)
1.詳解鎖升級(jí)過(guò)程
synchronized關(guān)鍵字在JDK1.6之前底層都是直接調(diào)用ObjectMonitor的enter和exit完成對(duì)操作系統(tǒng)級(jí)別的重量級(jí)鎖mutex的使用,這使得每次上鎖都需要從用戶態(tài)轉(zhuǎn)內(nèi)核態(tài)嘗試獲取重量級(jí)鎖的過(guò)程。
這種方式也不是不妥當(dāng),在并發(fā)度較高的場(chǎng)景下,取不到mutex的線程會(huì)因此直接阻塞,到等待隊(duì)列_WaitSet 中等待喚醒,而不是原地自選等待其他線程釋放鎖而立刻去爭(zhēng)搶,從而避免沒(méi)必要的線程原地自選等待導(dǎo)致的CPU開(kāi)銷,這也就是我們上文中講到的synchronized工作原理的過(guò)程。
但是在并發(fā)度較低的場(chǎng)景下,可能就10個(gè)線程,競(jìng)爭(zhēng)并不激烈可能線程等那么幾毫秒就可以拿到鎖了,而我們每個(gè)線程卻還是需要不斷從用戶態(tài)到內(nèi)核態(tài)獲取重量級(jí)鎖、到_WaitSet 中等待機(jī)會(huì)的過(guò)程,這種情況下,可能功能的開(kāi)銷還不如所競(jìng)爭(zhēng)的開(kāi)銷來(lái)得激烈。
所以JDK1.6之后,HotSpot虛擬機(jī)就對(duì)synchronized底層做了一定的優(yōu)化,通俗來(lái)說(shuō)根據(jù)線程競(jìng)爭(zhēng)的激烈程度的不斷增加逐步進(jìn)行鎖升級(jí)的策略。對(duì)應(yīng)的我們先給出32位虛擬機(jī)中不同級(jí)別的鎖在對(duì)象頭mark word中的標(biāo)識(shí)變化:
我們假設(shè)有這樣一個(gè)場(chǎng)景,我們有一個(gè)鎖對(duì)象LockObj,我們希望用它作為鎖,使用代碼邏輯如下所示:
synchronized(LockObj){
//dosomething
}
我們把自己當(dāng)作一個(gè)線程,一開(kāi)始沒(méi)有線程競(jìng)爭(zhēng)時(shí),synchronized鎖就是無(wú)鎖狀態(tài),無(wú)需進(jìn)行任何鎖爭(zhēng)搶的邏輯。此時(shí)鎖對(duì)象LockObj的偏向鎖標(biāo)志位為0,鎖標(biāo)記為01。
后續(xù)線程1需要嘗試執(zhí)行該語(yǔ)句塊,首先通過(guò)CAS修改mark word中的信息,即鎖的對(duì)象LockObj的對(duì)象頭偏向鎖標(biāo)記為1,鎖標(biāo)記為01,我們的線程開(kāi)始嘗試獲取這把鎖,并將線程id就當(dāng)前線程號(hào)即可。
后續(xù)線程1操作鎖時(shí),只需比較一下mark word中的鎖是否是偏向鎖且線程id是否是線程1即可:
當(dāng)我們發(fā)現(xiàn)偏向鎖中指向的線程id不是我們時(shí),就執(zhí)行下面的邏輯:
- 我們嘗試CAS競(jìng)爭(zhēng)這把鎖,如果成功則將鎖對(duì)象的markdown中的線程id設(shè)置為我們的線程id,然后執(zhí)行代碼邏輯。
- 我們嘗試CAS競(jìng)爭(zhēng)這把鎖失敗,則當(dāng)持有鎖的線程到達(dá)安全點(diǎn)的時(shí)候,直接將這個(gè)線程掛起并執(zhí)行鎖撤銷,將偏向鎖升級(jí)為輕量級(jí)鎖,然后持有鎖的線程繼續(xù)自己的邏輯,我們的線程繼續(xù)等待機(jī)會(huì)。
這里可能有讀者好奇什么叫安全點(diǎn)?
這里我們可以通俗的理解一下,安全點(diǎn)就是代碼執(zhí)行到的一個(gè)特殊位置,當(dāng)線程執(zhí)行到這個(gè)位置時(shí),我們可以將線程暫停下來(lái),讓我們?cè)跁和F陂g做一些處理。我們上文中將偏向鎖升級(jí)為輕量級(jí)鎖就是在安全點(diǎn)將線程暫停一下,將鎖升級(jí)為輕量級(jí)鎖,然后再讓線程進(jìn)行進(jìn)一步的工作。
升級(jí)為輕量級(jí)鎖時(shí),偏向鎖標(biāo)記為0,鎖標(biāo)記變?yōu)槭?0。此時(shí),如果我們的線程需要獲取這個(gè)輕量級(jí)鎖時(shí)的過(guò)程如下:
- 判斷當(dāng)前這把鎖是否為輕量級(jí)鎖,如果是則在線程棧幀中劃出一塊空間,存放這把鎖的信息,我們這里就把它稱為"鎖記錄",并將鎖對(duì)象的markword復(fù)制到鎖記錄中。
- 復(fù)制成功之后,通過(guò)CAS的方式嘗試將鎖對(duì)象頭中markword更新為鎖記錄的地址,并將owner指向鎖對(duì)象頭的markword。如果這幾個(gè)步驟操作成功,則說(shuō)明獲取輕量級(jí)鎖成功了。
- 如果線程CAS操作失敗,則進(jìn)行自旋獲取鎖,如果自旋超過(guò)10次(默認(rèn)設(shè)置為10次)還沒(méi)有得到鎖則將鎖升級(jí)為重量級(jí)鎖,升級(jí)為重量級(jí)鎖時(shí),鎖標(biāo)記為0,鎖狀態(tài)為10。由此導(dǎo)致持有鎖的線程進(jìn)行釋放時(shí)需要CAS修改mark word信息失敗,發(fā)現(xiàn)鎖已經(jīng)被其他線程膨脹為重量級(jí)鎖,對(duì)應(yīng)釋放操作改為將指針地址置空,然后喚醒其他等待的線程嘗試獲取鎖。
經(jīng)過(guò)上述的講解我們對(duì)鎖升級(jí)有了一個(gè)全流程的認(rèn)識(shí),在這里做個(gè)階段小結(jié):
- 無(wú)線程競(jìng)爭(zhēng),無(wú)鎖狀態(tài):偏向鎖標(biāo)記為0,鎖標(biāo)記為01。
- 存在一定線程競(jìng)爭(zhēng),大部分情況下會(huì)是同一個(gè)線程獲取到,升級(jí)為偏向鎖,偏向標(biāo)記為1,鎖標(biāo)記為01。
- 線程CAS爭(zhēng)搶偏向鎖鎖失敗,鎖升級(jí)為輕量級(jí)鎖,偏向標(biāo)記為0,鎖標(biāo)記為00。
- 線程原地自旋超過(guò)10次還未取得輕量級(jí)鎖,鎖升級(jí)為重量級(jí)鎖,避免大量線程原地自旋造成沒(méi)必要的CPU開(kāi)銷,偏向鎖標(biāo)記為0,鎖標(biāo)記為10。
2.基于jol-core代碼印證
上文我們將自己當(dāng)作一個(gè)線程了解完一次鎖升級(jí)的流程,口說(shuō)無(wú)憑,所以我們通過(guò)可以通過(guò)代碼來(lái)印證我們的描述。
上文講解鎖升級(jí)的之后,我們一直在說(shuō)對(duì)象頭的概念,所以為了能夠直觀的看到鎖對(duì)象中對(duì)象頭鎖標(biāo)記和鎖狀態(tài)的變化,我們這里引入一個(gè)jol工具。
<!--jol內(nèi)存分析工具-->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
然后我們聲明一下鎖對(duì)象作為實(shí)驗(yàn)對(duì)象。
public
class
Lock
{
private
int count;
public
int
getCount()
{
return count;
}
public
void
setCount(int count)
{
this.count = count;
}
}
首先是無(wú)鎖狀態(tài)的代碼示例,很簡(jiǎn)單,沒(méi)有任何線程爭(zhēng)搶邏輯,就通過(guò)jol工具打印鎖對(duì)象信息即可。
public
class
Lockless
{
public
static
void
main(String[] args)
{
Lock object=new Lock();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
打印結(jié)果如下,我們只需關(guān)注第一行的object header,可以看到第一列的00000001,我們看到后3位為001,偏向鎖標(biāo)記為0,鎖標(biāo)記為01,001這就是我們說(shuō)的無(wú)鎖狀態(tài)。
com.zsy.lock.lockUpgrade.Lock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0
4 (object header) 01
00
00
00 (00000001
00000000
00000000
00000000) (1)
4
4 (object header) 00
00
00
00 (00000000
00000000
00000000
00000000) (0)
8
4 (object header) 43 c1 00
20 (01000011
11000001
00000000
00100000) (536920387)
12
4
int Lock.count 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
接下來(lái)是偏向鎖,我們還是用同樣的代碼即可,需要注意的是偏向鎖必須在jvm啟動(dòng)后的一段時(shí)間才會(huì)運(yùn)行,所以如果我們想打印偏向鎖必須讓線程休眠那么幾秒,這里筆者就偷懶了一下,通過(guò)設(shè)置jvm參數(shù)-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0,通過(guò)禁止偏向鎖延遲,直接打印出偏向鎖信息
public
class
BiasLock
{
public
static
void
main(String[] args)
{
Lock object = new Lock();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
輸出結(jié)果如下,可以看到對(duì)象頭的信息為00000101,此時(shí)鎖標(biāo)記為1即偏向鎖標(biāo)記,鎖標(biāo)記為01,101即偏向鎖。
com.zsy.lock.lockUpgrade.Lock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0
4 (object header) 05
00
00
00 (00000101
00000000
00000000
00000000) (5)
4
4 (object header) 00
00
00
00 (00000000
00000000
00000000
00000000) (0)
8
4 (object header) 43 c1 00
20 (01000011
11000001
00000000
00100000) (536920387)
12
4
int Lock.count 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
然后的輕量級(jí)鎖的印證,我們只需使用Lock對(duì)象作為鎖即可。
public
class
LightweightLock
{
public
static
void
main(String[] args)
{
Lock object = new Lock();
synchronized (object) {
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
}
可以看到輕量級(jí)鎖鎖標(biāo)記為0,鎖標(biāo)記為00,000即輕量級(jí)。
com.zsy.lock.lockUpgrade.Lock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0
4 (object header) e8 f1 96
02 (11101000
11110001
10010110
00000010) (43446760)
4
4 (object header) 00
00
00
00 (00000000
00000000
00000000
00000000) (0)
8
4 (object header) 43 c1 00
20 (01000011
11000001
00000000
00100000) (536920387)
12
4
int Lock.count 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
最后就是重量級(jí)鎖了,我們只需打印出鎖對(duì)象的哈希碼即可將其升級(jí)為重量級(jí)鎖。
public
class
HeavyweightLock
{
public
static
void
main(String[] args)
{
Lock object = new Lock();
synchronized (object) {
System.out.println(object.hashCode());
}
synchronized (object) {
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
}
輸出結(jié)果為10001010,偏向鎖標(biāo)記為0,鎖標(biāo)記為10,010為重量級(jí)鎖。
1365202186
com.zsy.lock.lockUpgrade.Lock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0
4 (object header) 8a 15
83
17 (10001010
00010101
10000011
00010111) (394466698)
4
4 (object header) 00
00
00
00 (00000000
00000000
00000000
00000000) (0)
8
4 (object header) 43 c1 00
20 (01000011
11000001
00000000
00100000) (536920387)
12
4
int Lock.count 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
3.更多關(guān)于jol-core
jol不僅僅可以監(jiān)控Java進(jìn)程的鎖情況,在某些場(chǎng)景下,我們希望通過(guò)比較對(duì)象的地址來(lái)判斷當(dāng)前創(chuàng)建的實(shí)例是否是多例,是否存在線程安全問(wèn)題。此時(shí),我們就可以VM對(duì)象的方法獲取對(duì)象地址,如下所示:
public
static
void
main(String[] args) throws Exception {
//打印字符串a(chǎn)a的地址
System.out.println(VM.current().addressOf("aa"));
}
六、常見(jiàn)面試題
synchronized和ReentrantLock的區(qū)別
我們可以從三個(gè)角度來(lái)了解兩者的區(qū)別:
- 從實(shí)現(xiàn)角度:synchronized是JVM層面實(shí)現(xiàn)的鎖,ReentrantLock是屬于Java API層面實(shí)現(xiàn)的鎖,所以用起來(lái)需要我們手動(dòng)上鎖lock和釋放鎖unlock。
- 從性能角度:在JDK1.6之前可能ReentrantLock性能更好,在JDK1.6之后由于JVM對(duì)synchronized增加適應(yīng)性自旋鎖、鎖消除等策略的優(yōu)化使得synchronized和ReentrantLock性能并無(wú)太大的區(qū)別。
- 從功能角度:ReentrantLock相比于synchronized增加了更多的高級(jí)功能,例如等待可中斷、公平鎖、選擇性通知等功能。