您好,登錄后才能下訂單哦!
這篇文章主要介紹“JVM中Synchronized作用及原理是什么”,在日常操作中,相信很多人在JVM中Synchronized作用及原理是什么問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”JVM中Synchronized作用及原理是什么”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!
在 Java 中,如果要實(shí)現(xiàn)同步,Java 提供了一個(gè)關(guān)鍵詞 synchronized 來(lái)讓開(kāi)發(fā)人員可以快速實(shí)現(xiàn)同步代碼塊。
public class Test { public static void main(String[] args){ Object o = new Object(); Thread thread1 = new Thread(() -> { synchronized (o){ System.out.println("獲取鎖成功"); } }).start(); } }
線程 thread1 獲取對(duì)象 o 的鎖,并且輸出一句話 “獲取鎖成功”。
public class Test { private int i = 0; public synchronized void set(int i){ this.i = i; } public synchronized static String get(){ return "靜態(tài)方法"; } public void put(){ synchronized (this){ System.out.println("同步代碼塊"); } } }
synchronized 關(guān)鍵字除了可以用于代碼塊,還可以用于方法上。用于實(shí)例方法上時(shí),線程執(zhí)行該方法之前,會(huì)自動(dòng)獲取該對(duì)象鎖,獲取到對(duì)象鎖之后才會(huì)繼續(xù)執(zhí)行實(shí)例方法中的代碼;用于靜態(tài)方法上時(shí),線程執(zhí)行該方法之前,會(huì)自動(dòng)獲取該對(duì)象所屬類(lèi)的鎖,獲取到類(lèi)鎖之后才會(huì)繼續(xù)執(zhí)行靜態(tài)方法中的代碼。用于代碼塊上時(shí),可以傳入任意對(duì)象作為鎖,并且可以控制鎖的粒度。
下面是 Test 類(lèi)的字節(jié)碼文件
public class Test minor version: 0 major version: 55 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #7 // Test super_class: #8 // java/lang/Object interfaces: 0, fields: 1, methods: 4, attributes: 1 Constant pool: #1 = Methodref #8.#27 // java/lang/Object."<init>":()V #2 = Fieldref #7.#28 // Test.i:I #3 = String #29 // 靜態(tài)方法 #4 = Fieldref #30.#31 // java/lang/System.out:Ljava/io/PrintStream; #5 = String #32 // 同步代碼塊 #6 = Methodref #33.#34 // java/io/PrintStream.println:(Ljava/lang/String;)V #7 = Class #35 // Test #8 = Class #36 // java/lang/Object #9 = Utf8 i #10 = Utf8 I #11 = Utf8 <init> #12 = Utf8 ()V #13 = Utf8 Code #14 = Utf8 LineNumberTable #15 = Utf8 LocalVariableTable #16 = Utf8 this #17 = Utf8 LTest; #18 = Utf8 set #19 = Utf8 (I)V #20 = Utf8 get #21 = Utf8 ()Ljava/lang/String; #22 = Utf8 put #23 = Utf8 StackMapTable #24 = Class #37 // java/lang/Throwable #25 = Utf8 SourceFile #26 = Utf8 Test.java #27 = NameAndType #11:#12 // "<init>":()V #28 = NameAndType #9:#10 // i:I #29 = Utf8 靜態(tài)方法 #30 = Class #38 // java/lang/System #31 = NameAndType #39:#40 // out:Ljava/io/PrintStream; #32 = Utf8 同步代碼塊 #33 = Class #41 // java/io/PrintStream #34 = NameAndType #42:#43 // println:(Ljava/lang/String;)V #35 = Utf8 Test #36 = Utf8 java/lang/Object #37 = Utf8 java/lang/Throwable #38 = Utf8 java/lang/System #39 = Utf8 out #40 = Utf8 Ljava/io/PrintStream; #41 = Utf8 java/io/PrintStream #42 = Utf8 println #43 = Utf8 (Ljava/lang/String;)V { public Test(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_0 6: putfield #2 // Field i:I 9: return LineNumberTable: line 5: 0 line 7: 4 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this LTest; public synchronized void set(int); descriptor: (I)V flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #2 // Field i:I 5: return LineNumberTable: line 10: 0 line 11: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this LTest; 0 6 1 i I public static synchronized java.lang.String get(); descriptor: ()Ljava/lang/String; flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED Code: stack=1, locals=0, args_size=0 0: ldc #3 // String 靜態(tài)方法 2: areturn LineNumberTable: line 14: 0 public void put(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #5 // String 同步代碼塊 9: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 12: aload_1 13: monitorexit 14: goto 22 17: astore_2 18: aload_1 19: monitorexit 20: aload_2 21: athrow 22: return Exception table: from to target type 4 14 17 any 17 20 17 any LineNumberTable: line 18: 0 line 19: 4 line 20: 12 line 21: 22 LocalVariableTable: Start Length Slot Name Signature 0 23 0 this LTest; StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 17 locals = [ class Test, class java/lang/Object ] stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4 }
我們通過(guò)查看字節(jié)碼可以發(fā)現(xiàn),synchronized 關(guān)鍵字作用在實(shí)例方法和靜態(tài)方法上時(shí),JVM 是通過(guò) ACC_SYNCHRONIZED 這個(gè)標(biāo)志來(lái)實(shí)現(xiàn)同步的。而作用在代碼塊時(shí),而且通過(guò)指令 monitorenter 和 monitorexit 來(lái)實(shí)現(xiàn)同步的。monitorenter 是獲取鎖的指令,monitorexit 則是釋放鎖的指令。
通過(guò)上文我們已經(jīng)知道,Java 要實(shí)現(xiàn)同步,需要通過(guò)獲取對(duì)象鎖。那么在 JVM中,是如何知道哪個(gè)線程已經(jīng)獲取到了鎖呢?
要解釋這個(gè)問(wèn)題,我們首先需要了解一個(gè)對(duì)象的存儲(chǔ)分布由以下三部分組成:
對(duì)象頭(Header) :由 Mark Word 和 Klass Pointer 組成
實(shí)例數(shù)據(jù)(Instance Data) :對(duì)象的成員變量及數(shù)據(jù)
對(duì)齊填充(Padding) :對(duì)齊填充的字節(jié)
Mark Word ****記錄了對(duì)象運(yùn)行時(shí)的數(shù)據(jù):
identity_hashcode:哈希碼,只要獲取了才會(huì)有
age:GC分代年齡
biased_lock: 1表示偏向鎖,0表示非偏向鎖
lock 鎖狀態(tài) :01 無(wú)鎖/偏向鎖;00 輕量級(jí)鎖;10 重量級(jí)鎖;11 GC 標(biāo)志
偏向線程 ID
128bit (對(duì)象頭) | 狀態(tài) | ||||||
---|---|---|---|---|---|---|---|
64bit Mark Word | 64bit Klass Poiter | ||||||
unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | 無(wú)鎖 | |
threadId:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | 偏向鎖 | |
ptr_to_lock_record:62 | lock:2 | 輕量級(jí)鎖 | |||||
ptr_to_heavyweight_monitor:62 | lock:2 | 重量級(jí)鎖 | |||||
lock:2 | GC 標(biāo)記 |
當(dāng)線程獲取對(duì)象鎖的時(shí)候,需要先通過(guò)對(duì)象頭中的 Mark Word 判斷對(duì)象鎖是否已經(jīng)被其他線程獲取,如果沒(méi)有,那么線程需要往對(duì)象頭中寫(xiě)入一些標(biāo)記數(shù)據(jù),用于表示這個(gè)對(duì)象鎖已經(jīng)被我獲取了,其他線程無(wú)法再獲取到。如果對(duì)象鎖已經(jīng)被其他線程獲取了,那么線程就需要進(jìn)入到等待隊(duì)列中,直到持有鎖的線程釋放了鎖,它才有機(jī)會(huì)繼續(xù)獲取鎖。
當(dāng)一個(gè)線程擁有了鎖之后,它便可以多次進(jìn)入。當(dāng)然,在這個(gè)線程釋放鎖的時(shí)候,那么也需要執(zhí)行相同次數(shù)的釋放動(dòng)作。比如,一個(gè)線程先后3次獲得了鎖,那么它也需要釋放3次,其他線程才可以繼續(xù)訪問(wèn)。這也說(shuō)明使用 synchronized 獲取的鎖,都是可重入鎖。
我們知道了對(duì)象頭的內(nèi)存結(jié)構(gòu)之后,我們還需要了解一個(gè)很重要的概念:字節(jié)序。它表示每一個(gè)字節(jié)之間的數(shù)據(jù)在內(nèi)存中是如何存放的?如果不理解這個(gè)概念,那么在之后打印出對(duì)象頭時(shí),也會(huì)無(wú)法跟上述展示的對(duì)象頭內(nèi)存結(jié)構(gòu)相互對(duì)應(yīng)上。
字節(jié)序:大于一個(gè)字節(jié)的數(shù)據(jù)在內(nèi)存中的存放順序。
注意!注意!注意!這里使用了大于,也就是說(shuō)一個(gè)字節(jié)內(nèi)的數(shù)據(jù),它的順序是固定的。
大端序(BIG_ENDIAN):高位字節(jié)排在內(nèi)存的低地址處,低位字節(jié)排在內(nèi)存的高地址處。符合人類(lèi)的讀寫(xiě)順序
小端序(LITTLE_ENDIAN):高位字節(jié)排在內(nèi)存的高地址處,低位字節(jié)排在內(nèi)存的低地址處。符合計(jì)算機(jī)的讀取順序
我們來(lái)舉個(gè)例子:
有一個(gè)十六進(jìn)制的數(shù)字:0x123456789。
使用大端序閱讀:高位字節(jié)在前,低位字節(jié)在后。
內(nèi)存地址 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
十六進(jìn)制 | 0x01 | 0x23 | 0x45 | 0x67 | 0x89 |
二進(jìn)制 | 00000001 | 00100011 | 01000101 | 01100111 | 10001001 |
使用小端序閱讀:低位字節(jié)在前,高位字節(jié)在后。
內(nèi)存地址 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
十六進(jìn)制 | 0x89 | 0x67 | 0x45 | 0x23 | 0x01 |
二進(jìn)制 | 10001001 | 01100111 | 01000101 | 00100011 | 00000001 |
既然大端序符合人類(lèi)的閱讀習(xí)慣,那么統(tǒng)一使用大端序不就好了嗎?為什么還要搞出一個(gè)小端序來(lái)呢?
這是因?yàn)橛?jì)算機(jī)都是先從低位開(kāi)始處理的,這樣處理效率比較高,所以計(jì)算機(jī)內(nèi)部都是使用小端序。其實(shí)計(jì)算機(jī)也不知道什么是大端序,什么是小端序,它只會(huì)按順序讀取字節(jié),先讀第一個(gè)字節(jié),再讀第二個(gè)字節(jié)。
我們可以通過(guò)下面這一段代碼打印出 Java 的字節(jié)序:
public class ByteOrderPrinter { public static void main(String[] args){ System.out.println(ByteOrder.nativeOrder()); } }
打印的結(jié)果為: LITTLE_ENDIAN。
因此,我們可以知道 Java 中的字節(jié)序?yàn)?strong>小端字節(jié)序。
在理解了字節(jié)序之后,我們來(lái)看看如何閱讀對(duì)象頭。
首先,我們使用一個(gè)第三方類(lèi)庫(kù) jol-core,我使用的是 0.10 版本,幫助我們打印出對(duì)象頭的數(shù)據(jù)。
我們可以通過(guò)下面這一段代碼打印出 Java 的對(duì)象頭:
public class ObjectHeaderPrinter { public static void main(String[] args) throws InterruptedException { Test test = new Test(); System.out.println("=====打印匿名偏向鎖對(duì)象頭====="); System.out.println(ClassLayout.parseInstance(test).toPrintable()); synchronized (test){ System.out.println("=====打印偏向鎖對(duì)象頭====="); System.out.println(ClassLayout.parseInstance(test).toPrintable()); } } }
打印結(jié)果如下:
=====打印匿名偏向鎖/無(wú)鎖對(duì)象頭=====
Test 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) 50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
12 4 int Test.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total=====打印偏向鎖對(duì)象頭=====
Test object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 a0 80 4b (00000101 10100000 10000000 01001011) (1266720773)
4 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
8 4 (object header) 50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
12 4 int Test.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
我們把對(duì)象頭的內(nèi)存結(jié)構(gòu)和對(duì)象頭單獨(dú)拿出來(lái)對(duì)照著解釋一下:
128bit (對(duì)象頭) | 狀態(tài) | ||||||
---|---|---|---|---|---|---|---|
64bit Mark Word | 64bit Klass Poiter | ||||||
unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | 匿名偏向鎖/無(wú)鎖 | |
threadId:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | 偏向鎖 | |
ptr_to_lock_record:62 | lock:2 | 輕量級(jí)鎖 | |||||
ptr_to_heavyweight_monitor:62 | lock:2 | 重量級(jí)鎖 | |||||
lock:2 | GC 標(biāo)記 |
// 匿名偏向鎖/無(wú)鎖 // 我們給每個(gè)字節(jié)都標(biāo)上序號(hào)。 a b c d 05 00 00 00 (00000101 00000000 00000000 00000000) (5) e f g h 00 00 00 00 (00000000 00000000 00000000 00000000) (0) i j k l 50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
unused:25 位,它實(shí)際上的字節(jié)應(yīng)該是:hgf + e 的最高位。
identity_hashcode:31 位,它實(shí)際上的字節(jié)應(yīng)該是:e 的低 7 位 + dcb。
unused:1位,它實(shí)際上的字節(jié)應(yīng)該是:a 的最高位。
age:4位,它實(shí)際上的字節(jié)應(yīng)該是:a的第 4-7 位
biased_lock:1位,它實(shí)際上的字節(jié)應(yīng)該是:a的第 3 位
lock:2位,它實(shí)際上的字節(jié)應(yīng)該是:a的低 2 位。
unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |
---|---|---|---|---|---|
hgf + e的最高位 | e 的低 7 位 + dcb | a 的最高位 | a的第 4-7 位 | a的第 3 位 | a的低 2 位 |
00000000 00000000 00000000 0 | 0000000 00000000 00000000 00000000 | 0 | 0000 | 1 | 01 |
我們?cè)賮?lái)看一個(gè)加了偏向鎖的對(duì)象頭:
// 偏向鎖 a b c d 05 90 00 13 (00000101 10010000 00000000 00010011) (318803973) e f g h 01 00 00 00 (00000001 00000000 00000000 00000000) (1) i j k l 50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
threadId:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 |
---|---|---|---|---|---|
hgfedc + b 的高 6 位 | b的低 2 位 | a 的最高位 | a的第 4-7 位 | a的第 3 位 | a的低 2 位 |
00000000 00000000 00000000 00000001 00010011 00000000 100100 | 00 | 0 | 0000 | 1 | 01 |
偏向鎖是 Java 為了提高獲取鎖的效率和降低獲取鎖的代價(jià),而進(jìn)行的一個(gè)優(yōu)化。因?yàn)?Java 團(tuán)隊(duì)發(fā)現(xiàn)大多數(shù)的鎖都只被一個(gè)線程獲取。基于這種情況,就可以認(rèn)為鎖都只被一個(gè)線程獲取,那么就不會(huì)存在多個(gè)線程競(jìng)爭(zhēng)的條件,因此就可以不需要真正的去獲取一個(gè)完整的鎖。只需要在對(duì)象頭中寫(xiě)入獲取鎖的線程 ID,用于表示該對(duì)象鎖已經(jīng)被該線程獲取。
獲取偏向鎖,只要修改對(duì)象頭的標(biāo)記就可以表示線程已經(jīng)獲取了鎖,大大降低了獲取鎖的代價(jià)。
當(dāng)線程獲取對(duì)象的偏向鎖時(shí),它的對(duì)象頭:
threadId:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 |
---|
threadId:獲取了偏向鎖的線程 ID
epoch:用于保存偏向時(shí)間戳
age:對(duì)象 GC 年齡
biased_lock:偏向鎖標(biāo)記,此時(shí)為 1
lock:鎖標(biāo)記,此時(shí)為 10
線程獲取對(duì)象鎖時(shí),首先檢查對(duì)象鎖是否支持偏向鎖,即檢查 biased_lock 是否為 1;如果為 1,那么將會(huì)檢查threadId 是否為 null,如果為 null,將會(huì)通過(guò) CAS 操作將自己的線程 ID 寫(xiě)入到對(duì)象頭中。如果成功寫(xiě)入了線程 ID,那么該線程就獲取到了對(duì)象的偏向鎖,可以繼續(xù)執(zhí)行后面的同步代碼。
只有匿名偏向的對(duì)象才能進(jìn)入偏向鎖模式,即該對(duì)象還沒(méi)有偏向任何一個(gè)線程(不是絕對(duì)的,存在批量重偏向的情況)。
線程是不會(huì)主動(dòng)釋放偏向鎖的。只有當(dāng)其它線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放偏向鎖。
釋放偏向鎖需要在全局安全點(diǎn)進(jìn)行。釋放的步驟如下:
暫停擁有偏向鎖的線程,判斷是否處于同步代碼塊中,如果處于,則進(jìn)行偏向撤銷(xiāo),并升級(jí)為輕量級(jí)鎖。
如果不處于,則恢復(fù)為無(wú)鎖狀態(tài)。
由此可以知道,偏向鎖天然是可重入的。
偏向撤銷(xiāo)主要發(fā)生在多個(gè)線程存在競(jìng)爭(zhēng),不再偏向于任何一個(gè)線程了。也就是說(shuō)偏向撤銷(xiāo)之后,將不會(huì)再使用偏向鎖。具體操作就是將 Mark Work 中的 biased_lock 由 1 設(shè)置為 0 。 偏向撤銷(xiāo)需要到達(dá)全局安全點(diǎn)才可以撤銷(xiāo),因?yàn)樗枰薷膶?duì)象頭,并從棧中獲取數(shù)據(jù)。因此偏向撤銷(xiāo)也會(huì)存在較大的資源消耗。
想要撤銷(xiāo)偏向鎖,還不能對(duì)持有偏向鎖的線程有影響,所以就要等待持有偏向鎖的線程到達(dá)一個(gè) safepoint 安全點(diǎn)
,在這個(gè)安全點(diǎn)會(huì)掛起獲得偏向鎖的線程。
如果原持有偏向鎖的線程依然還在同步代碼塊中,那么就會(huì)將偏向鎖升級(jí)為輕量級(jí)鎖。
如果原持有偏向鎖的線程已經(jīng)死亡,或者已經(jīng)退出了同步代碼塊,那么直接撤銷(xiāo)偏向鎖狀態(tài)即可。
對(duì)象的偏向鎖被撤銷(xiāo)之后,對(duì)象在未來(lái)將不會(huì)偏向于任何一個(gè)線程。
我們可以想象,如果有 100 個(gè)對(duì)象都偏向于一個(gè)線程,此時(shí)如果有另外一個(gè)線程來(lái)獲取這些對(duì)象的鎖,那么這 100 個(gè)對(duì)象都會(huì)發(fā)生偏向撤銷(xiāo),而這 100 次偏向撤銷(xiāo)都需要在全局安全點(diǎn)下進(jìn)行,這樣就會(huì)產(chǎn)生大量的性能消耗。
批量重偏向就是建立在撤銷(xiāo)偏向會(huì)對(duì)性能產(chǎn)生較大影響情況下的一種優(yōu)化措施。當(dāng) JVM 知道有大量對(duì)象的偏向鎖撤銷(xiāo)時(shí),它就知道此時(shí)這些對(duì)象都不會(huì)偏向于原線程,所以會(huì)將對(duì)象重新偏向于新的線程,從而減少偏向撤銷(xiāo)的次數(shù)。
當(dāng)一個(gè)類(lèi)的大量對(duì)象被同一個(gè)線程 T1 獲取了偏向鎖,也就是大量對(duì)象先偏向于該線程 T1。T1 同步結(jié)束后,另一個(gè)線程 T2 對(duì)這些同一類(lèi)型的對(duì)象進(jìn)行同步操作,就會(huì)讓這些對(duì)象重新偏向于線程 T2。
在了解批量重偏向前,我們需要先了解一點(diǎn)其他知識(shí):
JVM 會(huì)給對(duì)象的類(lèi)對(duì)象 class 賦予兩個(gè)屬性,一個(gè)是偏向撤銷(xiāo)計(jì)數(shù)器,一個(gè)是 epoch 值。
我們先來(lái)看一個(gè)例子:
import org.openjdk.jol.info.ClassLayout; import java.util.ArrayList; import java.util.List; /** * @author liuhaidong * @date 2023/1/6 15:06 */ public class ReBiasTest { public static void main(String[] args) throws InterruptedException { //延時(shí)產(chǎn)生可偏向?qū)ο? //默認(rèn)4秒之后才能進(jìn)入偏向模式,可以通過(guò)參數(shù)-XX:BiasedLockingStartupDelay=0設(shè)置 Thread.sleep(5000); //創(chuàng)造100個(gè)偏向線程t1的偏向鎖 List<Test> listA = new ArrayList<>(); Thread t1 = new Thread(() -> { for (int i = 0; i < 100; i++) { Test a = new Test(); synchronized (a) { listA.add(a); } } try { //為了防止JVM線程復(fù)用,在創(chuàng)建完對(duì)象后,保持線程t1狀態(tài)為存活 Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } }); t1.start(); //睡眠3s鐘保證線程t1創(chuàng)建對(duì)象完成 Thread.sleep(3000); System.out.println("打印t1線程,list中第20個(gè)對(duì)象的對(duì)象頭:"); System.out.println((ClassLayout.parseInstance(listA.get(19)).toPrintable())); //創(chuàng)建線程t2競(jìng)爭(zhēng)線程t1中已經(jīng)退出同步塊的鎖 Thread t2 = new Thread(() -> { //這里面只循環(huán)了30次?。?! for (int i = 0; i < 30; i++) { Test a = listA.get(i); synchronized (a) { //分別打印第19次和第20次偏向鎖重偏向結(jié)果 if (i == 18 || i == 19) { System.out.println("第" + (i + 1) + "次偏向結(jié)果"); System.out.println((ClassLayout.parseInstance(a).toPrintable())); } if (i == 10) { // 該對(duì)象已經(jīng)是輕量級(jí)鎖,無(wú)法降級(jí),因此只能是輕量級(jí)鎖 System.out.println("第" + (i + 1) + "次偏向結(jié)果"); System.out.println((ClassLayout.parseInstance(a).toPrintable())); } } } try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } }); t2.start(); Thread.sleep(3000); System.out.println("打印list中第11個(gè)對(duì)象的對(duì)象頭:"); System.out.println((ClassLayout.parseInstance(listA.get(10)).toPrintable())); System.out.println("打印list中第26個(gè)對(duì)象的對(duì)象頭:"); System.out.println((ClassLayout.parseInstance(listA.get(25)).toPrintable())); System.out.println("打印list中第41個(gè)對(duì)象的對(duì)象頭:"); System.out.println((ClassLayout.parseInstance(listA.get(40)).toPrintable())); } }
在 JDK8 中,-XX:BiasedLockingStartupDelay 的默認(rèn)值是 4000;在 JDK11 中,-XX:BiasedLockingStartupDelay 的默認(rèn)值是 0
t1 執(zhí)行完后,100 個(gè)對(duì)象都會(huì)偏向于 t1。
t2 執(zhí)行完畢之后,其中前 19 個(gè)對(duì)象都會(huì)撤銷(xiāo)偏向鎖,此時(shí)類(lèi)中的偏向撤銷(xiāo)計(jì)數(shù)器為19。但當(dāng)撤銷(xiāo)到第 20 個(gè)的時(shí)候,偏向撤銷(xiāo)計(jì)數(shù)器為 20,此時(shí)達(dá)到 -XX:BiasedLockingBulkRebiasThreshold=20
的條件,于是將類(lèi)中的 epoch 值 +1,并在此時(shí)找到所有處于同步代碼塊的對(duì)象,并將其 epoch 值等于類(lèi)對(duì)象的 epoch 值。然后進(jìn)行批量重偏向操作,從第 20 個(gè)對(duì)象開(kāi)始,將會(huì)比較對(duì)象的 epoch 值是否等于類(lèi)對(duì)象的 epoch 值,如果不等于,那么直接使用 CAS 替換掉 Mark Word 中的程 ID 為當(dāng)前線程的 ID。
結(jié)論:
前 19 個(gè)對(duì)象撤銷(xiāo)了偏向鎖,即 Mark Word 中的 biased_lock 為 0,如果有線程來(lái)獲取鎖,那么先獲取輕量級(jí)鎖。
第 20 - 30 個(gè)對(duì)象,依然為偏向鎖,偏向于線程 t2。
第 31 - 100 個(gè)對(duì)象,依然為偏向鎖,偏向于線程 t1。
tech.youzan.com/javasuo-yu-…
暫時(shí)無(wú)法在飛書(shū)文檔外展示此內(nèi)容
當(dāng)偏向鎖撤銷(xiāo)的數(shù)量達(dá)到 40 時(shí),就會(huì)發(fā)生批量撤銷(xiāo)。但是,這是在一個(gè)時(shí)間范圍內(nèi)達(dá)到 40 才會(huì)發(fā)生,這個(gè)時(shí)間范圍通過(guò) -XX:BiasedLockingDecayTime
設(shè)置,默認(rèn)值為 25 秒。
也就是在發(fā)生批量偏向的 25 秒內(nèi),如果偏向鎖撤銷(xiāo)的數(shù)量達(dá)到了 40 ,那么就會(huì)發(fā)生批量撤銷(xiāo),將該類(lèi)下的所有對(duì)象都進(jìn)行撤銷(xiāo)偏向,包括后續(xù)創(chuàng)建的對(duì)象。如果在發(fā)生批量偏向的 25 秒內(nèi)沒(méi)有達(dá)到 40 ,就會(huì)重置偏向鎖撤銷(xiāo)數(shù)量,將偏向鎖撤銷(xiāo)數(shù)量重置為 20。
我們通過(guò) Mark Word 知道,在無(wú)鎖狀態(tài)下,如果調(diào)用對(duì)象的 hashcode()
方法,就會(huì)在 Mark Word 中記錄對(duì)象的 Hashcode 值,在下一次調(diào)用 hashcode()
方法時(shí),就可以直接通過(guò) Mark Word 來(lái)得知,而不需要再次計(jì)算,以此來(lái)保證 Hashcode 的一致性。
但是獲取了鎖之后,就會(huì)修改 Mark Word 中的值,那么之前記錄下來(lái)的 Hashcode 值去哪里了呢?
在解答這個(gè)問(wèn)題之前,我們需要先知道一個(gè)東西:Lock Record。
當(dāng)字節(jié)碼解釋器執(zhí)行 monitorenter 字節(jié)碼輕度鎖住一個(gè)對(duì)象時(shí),就會(huì)在獲取鎖的線程棧上顯式或者隱式分配一個(gè) Lock Record。換句話說(shuō),就是在獲取輕量級(jí)鎖時(shí),會(huì)在線程棧上分配一個(gè) Lock Record。這個(gè) Lock Record 說(shuō)直白一點(diǎn)就是棧上的一塊空間,主要用于存儲(chǔ)相關(guān)信息。
Lock Record 只要有三個(gè)作用:
持有 Displaced Word(就是對(duì)象的 Mark Word)和一些元信息用于識(shí)別哪個(gè)對(duì)象被鎖住了。
解釋器使用 Lock Record 來(lái)檢測(cè)非法的鎖狀態(tài)
隱式地充當(dāng)鎖重入機(jī)制的計(jì)數(shù)器
那么這個(gè) Lock Record 跟 Hashcode 有什么關(guān)系呢?
我們先來(lái)看第一個(gè)場(chǎng)景:先獲取對(duì)象的 hashcode,然后再獲取對(duì)象的鎖。
import org.openjdk.jol.info.ClassLayout; public class TestObject { public static void main(String[] args) { Test test = new Test(); // 步驟 1 System.out.println("=====獲取 hashcode 之前====="); System.out.println(ClassLayout.parseInstance(test).toPrintable()); test.hashCode(); // 步驟 2 System.out.println("=====獲取 hashcode 之后====="); System.out.println(ClassLayout.parseInstance(test).toPrintable()); // 步驟 3 synchronized (test){ System.out.println("=====獲取鎖之后====="); System.out.println(ClassLayout.parseInstance(test).toPrintable()); } // 步驟 4 System.out.println("=====釋放鎖之后====="); System.out.println(ClassLayout.parseInstance(test).toPrintable()); } }
運(yùn)行結(jié)果:
=====獲取 hashcode 之前=====
Test 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) 50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
12 4 int Test.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total=====獲取 hashcode 之后=====
Test object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 0c 97 8b (00000001 00001100 10010111 10001011) (-1953035263)
4 4 (object header) 76 00 00 00 (01110110 00000000 00000000 00000000) (118)
8 4 (object header) 50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
12 4 int Test.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total=====獲取鎖之后=====
Test object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 90 2a 90 6b (10010000 00101010 10010000 01101011) (1804610192)
4 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
8 4 (object header) 50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
12 4 int Test.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total=====釋放鎖之后=====
Test object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 0c 97 8b (00000001 00001100 10010111 10001011) (-1953035263)
4 4 (object header) 76 00 00 00 (01110110 00000000 00000000 00000000) (118)
8 4 (object header) 50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
12 4 int Test.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
步驟一:未獲取對(duì)象的 hashcode 值之前,對(duì)象處于匿名偏向鎖狀態(tài)。鎖標(biāo)記為:101
步驟二:獲取對(duì)象的 hashcode 之后,對(duì)象的偏向狀態(tài)被撤銷(xiāo),處于無(wú)鎖狀態(tài)。鎖標(biāo)記為:001。對(duì)象頭中也存儲(chǔ)了 hashcode 值,hashcode 值為 0111011 10001011 10010111 00001100。
步驟三:獲取鎖之后,對(duì)象處于輕量級(jí)鎖狀態(tài)。鎖標(biāo)記為:00。其余 62 位為指向 Lock Record 的指針。從這里我們可以看到,Mark Word 中已經(jīng)沒(méi)有 hashcode 了。整塊 Mark Word 的內(nèi)容已經(jīng)被復(fù)制到 Lock Word 中。
步驟四:釋放鎖之后,對(duì)象處于無(wú)鎖狀態(tài)。鎖標(biāo)記為:001。在 Mark Word 中也可以看到之前生成的 hashcode。與步驟二中的 Mark Word 一模一樣。這是因?yàn)樵卺尫沛i之后,JVM 會(huì)將 Lock Record 中的值復(fù)制回 Mark Word 中,并刪除 Lock Record。
結(jié)論:
當(dāng)對(duì)象生成 hashcode 之后,會(huì)撤銷(xiāo)偏向,并將 hashcode 記錄在 Mark Word 中。
非偏向的對(duì)象獲取鎖時(shí),會(huì)先在棧中生成一個(gè) Lock Record。并將對(duì)象的 Mark Word 復(fù)制到 Lock Record 中。
我們現(xiàn)在來(lái)看第二個(gè)場(chǎng)景:先獲取對(duì)象的鎖,然后在同步代碼塊中生成 hashcode。
import org.openjdk.jol.info.ClassLayout; public class HashCode2 { public static void main(String[] args) { Test test = new Test(); // 步驟一 System.out.println("=====獲取鎖之前====="); System.out.println(ClassLayout.parseInstance(test).toPrintable()); synchronized (test){ // 步驟二 System.out.println("=====獲取鎖之后,獲取hashcode之前====="); System.out.println(ClassLayout.parseInstance(test).toPrintable()); // 步驟三 test.hashCode(); System.out.println("=====獲取鎖之后,獲取hashcode之后====="); System.out.println(ClassLayout.parseInstance(test).toPrintable()); } // 步驟四 System.out.println("=====釋放鎖之后====="); System.out.println(ClassLayout.parseInstance(test).toPrintable()); } }
運(yùn)行結(jié)果:
=====獲取鎖之前=====
Test 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) 50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
12 4 int Test.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total=====獲取鎖之后,獲取hashcode之前=====
Test object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 90 80 3a (00000101 10010000 10000000 00111010) (981504005)
4 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
8 4 (object header) 50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
12 4 int Test.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total=====獲取鎖之后,獲取hashcode之后=====
Test object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 02 e8 83 2a (00000010 11101000 10000011 00101010) (713287682)
4 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
8 4 (object header) 50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
12 4 int Test.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total=====釋放鎖之后=====
Test object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 02 e8 83 2a (00000010 11101000 10000011 00101010) (713287682)
4 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
8 4 (object header) 50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
12 4 int Test.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
步驟一:未獲取對(duì)象的 hashcode 值之前,對(duì)象處于匿名偏向鎖狀態(tài)。鎖標(biāo)記為:101
步驟二:進(jìn)入同步代碼塊,線程獲取了偏向鎖。鎖標(biāo)記:101
步驟三:對(duì)象生成 hashcode,此時(shí)鎖標(biāo)記:10,直接從偏向鎖升級(jí)為重量級(jí)鎖。 其余 62 位為指向 objectMonitor 的指針。
與輕量級(jí)鎖存在同樣的問(wèn)題,hashcode 會(huì)存放在哪里?每一個(gè)對(duì)象在 JVM 中都有一個(gè) objectMonitor 對(duì)象,而 Mark Word 就存儲(chǔ)在 objectMonitor 對(duì)象的 header 屬性中。
輕量級(jí)鎖解決的場(chǎng)景是:任意兩個(gè)線程交替獲取鎖的情況。主要依靠 CAS 操作,相比較于使用重量級(jí)鎖,可以減少鎖資源的消耗。
使用輕量級(jí)鎖的情況有以下幾種:
禁用偏向鎖。
偏向鎖失效,升級(jí)為輕量級(jí)鎖。
禁用偏向鎖導(dǎo)致升級(jí)
在啟動(dòng) Java 程序時(shí),如果添加了 JVM 參數(shù) -XX:-UseBiasedLocking
, 那么在后續(xù)的運(yùn)行中,就不再使用偏向鎖 。
偏向鎖失效,升級(jí)為輕量級(jí)鎖
如果對(duì)象發(fā)生偏向撤銷(xiāo)時(shí):
首先會(huì)檢查持有偏向鎖的線程是否已經(jīng)死亡,如果死亡,則直接升級(jí)為輕量級(jí)鎖,否則,執(zhí)行步驟2
查看持有偏向鎖的線程是否在同步代碼塊中,如果在,則將偏向鎖升級(jí)為輕量級(jí)鎖,否則,執(zhí)行步驟3
修改 Mark Word 為非偏向模式,設(shè)置為無(wú)鎖狀態(tài)。
當(dāng)線程獲取輕量級(jí)鎖時(shí),首先會(huì)在線程棧中創(chuàng)建一個(gè) Lock Record 的內(nèi)存空間,然后拷貝 Mark Word 中的數(shù)據(jù)到 Lock Record 中。JVM 中將有數(shù)據(jù)的 Lock Record 叫做 Displated Mark Word。
Lock Record 在棧中的內(nèi)存結(jié)構(gòu):
暫時(shí)無(wú)法在飛書(shū)文檔外展示此內(nèi)容
當(dāng)數(shù)據(jù)復(fù)制成功之后,JVM 將會(huì)使用 CAS 嘗試修改 Mark Word 中的數(shù)據(jù)為指向線程棧中 Displated Mark Word 的指針,并將 Lock Record 中的 owner 指針指向 Mark Word。
如果這兩步操作都更新成功了,那么則表示該線程獲得輕量級(jí)鎖成功,設(shè)置 Mark Word 中的 lock 字段為 00,表示當(dāng)前對(duì)象為輕量級(jí)鎖狀態(tài)。同步,線程可以執(zhí)行同步代碼塊。
如果更新操作失敗了,那么 JVM 將會(huì)檢查 Mark Word 是否指向當(dāng)前線程的棧幀:
如果是,則表示當(dāng)前線程已經(jīng)獲取了輕量級(jí)鎖,會(huì)在棧幀中添加一個(gè)新的 Lock Record,這個(gè)新 Lock Record 中的 Displated Mark Word 為 null,owner 指向?qū)ο?。這樣的目的是為了統(tǒng)計(jì)重入的鎖數(shù)量,因此,在棧中會(huì)有一個(gè) Lock Record 的列表。完成這一步之后就可以直接執(zhí)行同步代碼塊。
暫時(shí)無(wú)法在飛書(shū)文檔外展示此內(nèi)容
如果不是,那么表示輕量級(jí)鎖發(fā)生競(jìng)爭(zhēng),后續(xù)將會(huì)膨脹為重量級(jí)鎖。
釋放輕量級(jí)鎖時(shí),會(huì)在棧中由低到高,獲取 Lock Record。查詢(xún)到 Lock Record 中的 Displated Mark Word 為 null 時(shí),則表示,該鎖是重入的,只需要將 owner 設(shè)置為 null 即可,表示已經(jīng)釋放了這個(gè)鎖。如果 Displated Mark Word 不為 null,則需要通過(guò) CAS 將 Displated Mark Word 拷貝至對(duì)象頭的 Mark Word 中,然后將 owner 的指針設(shè)置為 null,最后修改 Mark Word 的 lock 字段為 01 無(wú)鎖狀態(tài)。
重量級(jí)鎖解鎖的場(chǎng)景是:多個(gè)線程相互競(jìng)爭(zhēng)同一個(gè)鎖。主要通過(guò) park()
和 unpark()
方法,結(jié)合隊(duì)列來(lái)完成。相較于輕量級(jí)鎖和偏向鎖,需要切換內(nèi)核態(tài)和用戶態(tài)環(huán)境,因此獲取鎖的過(guò)程會(huì)消耗較多的資源。
使用重量級(jí)鎖的情況有兩種:
在持有偏向鎖的情況下,直接獲取對(duì)象的 hashcode,將會(huì)直接升級(jí)為重量級(jí)鎖。
在輕量級(jí)鎖的情況下,存在競(jìng)爭(zhēng),膨脹為重量級(jí)鎖。
獲取 hashcode,升級(jí)為重量級(jí)鎖
import org.openjdk.jol.info.ClassLayout; public class HashCode2 { public static void main(String[] args) { Test test = new Test(); // 步驟一 System.out.println("=====獲取鎖之前====="); System.out.println(ClassLayout.parseInstance(test).toPrintable()); synchronized (test){ // 步驟二 System.out.println("=====獲取鎖之后,獲取hashcode之前====="); System.out.println(ClassLayout.parseInstance(test).toPrintable()); // 步驟三 test.hashCode(); System.out.println("=====獲取鎖之后,獲取hashcode之后====="); System.out.println(ClassLayout.parseInstance(test).toPrintable()); } } }
執(zhí)行后的結(jié)果
=====獲取鎖之前=====
Test 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) 50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
12 4 int Test.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total=====獲取鎖之后,獲取hashcode之前=====
Test object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 90 80 3a (00000101 10010000 10000000 00111010) (981504005)
4 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
8 4 (object header) 50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
12 4 int Test.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total=====獲取鎖之后,獲取hashcode之后=====
Test object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 02 e8 83 2a (00000010 11101000 10000011 00101010) (713287682)
4 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
8 4 (object header) 50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
12 4 int Test.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
我們直接在偏向鎖的同步代碼塊中執(zhí)行 hashcode()
,會(huì)發(fā)現(xiàn)偏向鎖直接膨脹為重量級(jí)鎖了。我們可以看到 lock 字段為 10。
這里有一個(gè)疑問(wèn),為什么不是升級(jí)為輕量級(jí)鎖呢?輕量級(jí)鎖也可以在 Lock Record 中存儲(chǔ)生成的 hashcode。而膨脹為更為消耗資源的重量級(jí)鎖。
輕量級(jí)鎖膨脹為重量級(jí)鎖
當(dāng)處于輕量級(jí)鎖的時(shí)候,說(shuō)明鎖已經(jīng)不再偏向于任何一個(gè)線程,但是也沒(méi)有發(fā)生競(jìng)爭(zhēng),可以依靠 CAS 獲取到輕量級(jí)鎖。但是當(dāng)出現(xiàn) CAS 獲取鎖失敗時(shí),就會(huì)直接膨脹為重量級(jí)鎖。
這里需要注意,只會(huì) CAS 一次,只要一次失敗就會(huì)直接膨脹為重量級(jí)鎖,而不是達(dá)到自旋次數(shù)或者自旋時(shí)間才膨脹。
在膨脹過(guò)程中,會(huì)有幾種標(biāo)記來(lái)表示鎖的狀態(tài):
Inflated:膨脹已完成
Stack-locked:輕量級(jí)鎖
INFLATING:膨脹中
Neutral:無(wú)鎖
膨脹步驟:
檢查是否已經(jīng)為重量級(jí)鎖,如果是直接返回。
檢查是否處于膨脹中的狀態(tài),如果是,循環(huán)檢測(cè)狀態(tài)。檢測(cè)出膨脹中的狀態(tài)是因?yàn)橛衅渌€程正在進(jìn)行膨脹,因?yàn)樾枰却蛎浲瓿芍?,才能繼續(xù)執(zhí)行。
檢查是否為輕量級(jí)鎖,如果是,則執(zhí)行以下步驟:
創(chuàng)建一個(gè) ObjectMonitor 對(duì)象。
通過(guò) CAS 設(shè)置 Mark Word 為全 0,用以表示 INFLATING 狀態(tài)。如果失敗,則從步驟 1 重新開(kāi)始執(zhí)行。
將 Mark Word 設(shè)置到 ObjectMonitor 對(duì)象中。
設(shè)置 owner 屬性為 Lock Record
設(shè)置 Mark Word 值
返回
判定為無(wú)鎖狀態(tài),執(zhí)行以下步驟:
創(chuàng)建一個(gè) ObjectMonitor 對(duì)象。
通過(guò) CAS 直接設(shè)置 Mark Word 值。
返回
我們要理解如何獲取重量級(jí)鎖,需要先了解 ObjectMonitor 對(duì)象。顧名思義,這是一個(gè)對(duì)象監(jiān)視器。在 Java 中,每個(gè)對(duì)象都有一個(gè)與之對(duì)應(yīng)的 ObjectMonitor 。ObjectMonitor 內(nèi)部有幾個(gè)重要的字段:
cxq:存放被阻塞的線程
EntryList:存放被阻塞的線程,在釋放鎖時(shí)使用
WaitSet:獲得鎖的線程,如果調(diào)用 wait() 方法,那么線程會(huì)被存放在此處,這是一個(gè)雙向循環(huán)鏈表
onwer:持有鎖的線程
cxq,EntryList 均為 ObjectWaiter 類(lèi)型的單鏈表。
獲取鎖過(guò)程
通過(guò) CAS 設(shè)置 onwer 為當(dāng)前線程(嘗試獲取鎖),CAS 的原值為 NULL,新值為 current_thread,如果成功,則表示獲得鎖。否則執(zhí)行步驟 2
判斷當(dāng)前線程與獲取鎖線程是否一致,如果一致,則表示獲得鎖(鎖重入)。否則執(zhí)行步驟 3
判斷當(dāng)前線程是否為之前持有輕量級(jí)鎖的線程,如果是,直接設(shè)置 onwer 為當(dāng)前線程,表示獲得鎖。否則執(zhí)行步驟 4
以上步驟都失敗,則嘗試一輪自旋來(lái)獲取鎖。如果未獲取鎖,則執(zhí)行步驟 5
使用阻塞和喚醒來(lái)控制線程競(jìng)爭(zhēng)鎖
通過(guò) CAS 設(shè)置 owner 為當(dāng)前線程(嘗試獲取鎖),CAS 的原值為 NULL,新值為 current_thread。如果成功,則表示獲得鎖。否則執(zhí)行步驟 b
通過(guò) CAS 設(shè)置 owner 為當(dāng)前線程(嘗試獲取鎖)CAS 的原值為 DEFLATER_MARKER,新值為 current_thread。如果成功,則表示獲得鎖。否則執(zhí)行步驟c。(DEFLATER_MARKER 是一個(gè)鎖降級(jí)的標(biāo)記,后續(xù)會(huì)講解。)
以上步驟都失敗,則嘗試一輪自旋來(lái)獲取鎖。如果未獲取鎖,則執(zhí)行步驟 d。
為當(dāng)前線程創(chuàng)建一個(gè) ObjectWaiter 類(lèi)型的 node 節(jié)點(diǎn)。步驟 i 和 ii 是一個(gè)循環(huán),直到一個(gè)成功才會(huì)跳出這個(gè)循環(huán)。
通過(guò) cas 插入 cxq 的頭部,如果插入失敗,則執(zhí)行步驟 ii
通過(guò) CAS 設(shè)置 owner 為當(dāng)前線程(嘗試獲取鎖),CAS 的原值為 NULL,新值為 current_thread。如果失敗,則執(zhí)行 i。
通過(guò) CAS 設(shè)置 owner 為當(dāng)前線程(嘗試獲取鎖),CAS 的原值為 NULL,新值為 current_thread。如果成功,則表示獲得鎖。否則執(zhí)行步驟 f。(該步驟往下開(kāi)始是一個(gè)循環(huán),直到獲取到鎖為止)
通過(guò) park(),將線程阻塞。
線程被喚醒后
通過(guò) CAS 設(shè)置 owner 為當(dāng)前線程(嘗試獲取鎖),CAS 的原值為 NULL,新值為 current_thread。如果成功,則表示獲得鎖。否則執(zhí)行步驟 ii
通過(guò) CAS 設(shè)置 owner 為當(dāng)前線程(嘗試獲取鎖)CAS 的原值為 DEFLATER_MARKER,新值為 current_thread。如果成功,則表示獲得鎖。否則執(zhí)行 iii
嘗試一輪自旋來(lái)獲取鎖。如果未獲取鎖,則跳轉(zhuǎn)回步驟 e 執(zhí)行。
自適應(yīng)自旋鎖主要是用于重量級(jí)鎖中,降低阻塞線程概率。而不是用于輕量級(jí)鎖,這里大家要多多注意。
判斷 _owner 字段是否等于 current_thread。如果等于則判斷當(dāng)前線程是否為持有輕量級(jí)鎖的線程,如果是的話,表示該線程還沒(méi)有執(zhí)行 enter()
方法,因此,直接設(shè)置 _owner 字段為 current_thread。
判斷 _recursions,如果大于0,則表示鎖重入,直接返回即可,不需要執(zhí)行后續(xù)解鎖代碼。
設(shè)置 _owner 字段為 NULL,解鎖成功,后續(xù)線程可以正常獲取到鎖。
喚醒其他正在被阻塞的線程。在執(zhí)行以下操作之前需要使用該線程重新獲取鎖。如果獲取鎖失敗,則表示鎖已經(jīng)被其他線程獲取,直接返回,不再喚醒其他線程。(為什么還要獲取到鎖才可以喚醒其他線程呢?因?yàn)閱拘丫€程時(shí),需要將 cxq 中的節(jié)點(diǎn)轉(zhuǎn)移到 EntryList 中,涉及到鏈表的移動(dòng),如果多線程執(zhí)行,將會(huì)出錯(cuò)。)
如何 _EntryList 非空,那么取 _EntryList 中的第一個(gè)元素,將該元素下的線程喚醒。否則執(zhí)行步驟 b。
將 _cxq 設(shè)置為空,并將 _cxq 的元素按照原順序放入 _EntryList 中。然后取 _EntryList 中的第一個(gè)元素,將該元素下的線程喚醒。
線程喚醒
設(shè)置 _owner 字段為 NULL,解鎖成功,讓后續(xù)線程可以正常獲取到鎖。
然后調(diào)用 unpark()
方法,喚醒線程。
我們需要知道一個(gè)前提,在處理 wait
方法時(shí),必須使用重量級(jí)鎖。因此,wait
方法會(huì)導(dǎo)致鎖升級(jí)。
我們先來(lái)看一個(gè)例子:
public class WaitTest { static final Object lock = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (lock){ log("get lock"); try { log("wait lock"); lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } log("get lock again"); log("release lock"); } }, "thread-A").start(); sleep(1000); new Thread(() -> { synchronized (lock){ log("get lock"); createThread("thread-C"); sleep(2000); log("start notify"); lock.notify(); log("release lock"); } }, "thread-B").start(); } public static void createThread(String threadName) { new Thread(() -> { synchronized (lock){ log("get lock"); log("release lock"); } }, threadName).start(); } private static void sleep(long sleepVal){ try{ Thread.sleep(sleepVal); }catch(Exception e){ e.printStackTrace(); } } private static void log(String desc){ System.out.println(Thread.currentThread().getName() + " : " + desc); } }
最后打印的結(jié)果:
thread-A : get lock
thread-A : wait lock
thread-B : get lock
thread-B : start notify
thread-B : release lock
thread-A : get lock again
thread-A : release lock
thread-C : get lock
thread-C : release lock
線程 A 首先獲取到鎖,然后通過(guò) wait()
方法,將鎖釋放,并且等待通知。
睡眠 1 S,這里是確保線程 A 可以順利完成所有操作。
因?yàn)?A 釋放了鎖,所以線程 B 可以獲取到鎖。然后創(chuàng)建了線程 C。
因?yàn)榫€程 B 睡眠了 2S,依然持有鎖,所以線程 C 無(wú)法獲取到鎖,只能繼續(xù)等待。
線程 B 調(diào)用 notify()
方法,線程 A 被喚醒,開(kāi)始競(jìng)爭(zhēng)鎖。
線程 A 和線程 C 競(jìng)爭(zhēng)鎖。
但是根據(jù)打印結(jié)果,無(wú)論執(zhí)行多少次,都是線程 A 先獲取鎖。
第一個(gè)問(wèn)題:為什么都是線程 A 先獲取鎖,而不是線程 C 先獲取鎖?
第二個(gè)問(wèn)題:為什么 wait 方法并沒(méi)有生成 monitorenter 指令,也可以獲取到鎖?
第三個(gè)問(wèn)題:執(zhí)行 wait 之后,線程去哪里了?它的狀態(tài)是什么?
為了解答這些問(wèn)題,我們需要深入到源碼中去。但是這里就不放源碼了,我只講一下關(guān)鍵步驟:
wait()
膨脹為重量級(jí)鎖
為 current_thread 創(chuàng)建 ObjectWaiter 類(lèi)型的 node 節(jié)點(diǎn)
將 node 放入 _waitSet 中
釋放鎖
通過(guò) park() 阻塞 current_thread。
notify()
檢查 _waitSet 是否為 null,如果為 null,直接返回
獲取 _waitSet 的第一個(gè)元素 node,并將其從鏈表中移除。
此時(shí),存在三個(gè)策略:默認(rèn)使用 policy = 2
插入到 EntryList 的頭部(policy = 1)
插入到 EntryList 的尾部(policy = 0)
插入到 cxq 的 頭部(policy = 2)
將 node 插入到 cxq 的頭部。
notifyAll()
循環(huán)檢測(cè) _waitSet 是否不為空
如果不為空,則執(zhí)行 notify() 的步驟。
否則返回
第一個(gè)問(wèn)題:執(zhí)行 wait 之后,線程去哪里了?它的狀態(tài)是什么?
線程 A 調(diào)用 wait() 方法后,線程 A 就被 park 了,并被放入到 _waitSet 中。此時(shí)他的狀態(tài)就是 WAITING。如果它從 _waitSet 移除,并被放入到 cxq 之后,那么他的狀態(tài)就會(huì)變?yōu)?BLOCKED。如果它競(jìng)爭(zhēng)到鎖,那么他的狀態(tài)就會(huì)變?yōu)?RUNNABLE 。
第二個(gè)問(wèn)題:為什么 wait 方法并沒(méi)有生成 monitorenter 指令,也可以獲取到鎖?
線程 A 調(diào)用 wait() 方法后,線程 A 被放入到 _waitSet 中。直到有其他線程調(diào)用 notify() 之后,線程 A 從 _waitSet 移除,并放入到 cxq 中。
第三個(gè)問(wèn)題:為什么都是線程 A 先獲取鎖,而不是線程 C 先獲取鎖?
線程 A 調(diào)用 wait() 方法后,線程 A 被放入到 _waitSet 中。線程 B 獲取鎖,然后創(chuàng)建了線程 C,線程 C 競(jìng)爭(zhēng)鎖失敗,被放入到 cxq 中。然后 B 調(diào)用 notify() 方法后,線程 A 從 _waitSet 移除,放入到 cxq 的頭部。因此目前 cxq 的鏈表結(jié)構(gòu)為:A -> C -> null。接著線程 B 釋放鎖,會(huì)將 cxq 中的元素按照原順序放入到 EntryList 中,因此目前 cxq 鏈表結(jié)構(gòu)為:null;EntryList 鏈表結(jié)構(gòu)為:A -> C -> null。然后喚醒 EntryList 中的第一個(gè)線程。
所以,每次都是線程 A 先獲取鎖。
到此,關(guān)于“JVM中Synchronized作用及原理是什么”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。