欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

從?JVM?中深入探究?Synchronized作用及原理

 更新時(shí)間:2023年03月30日 14:55:24   作者:好學(xué)的康達(dá)姆機(jī)器人  
這篇文章主要為大家介紹了從?JVM?中深入探究?Synchronized作用及原理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

開篇語

Synchronized,Java 友好的提供了的一個(gè)關(guān)鍵字,它讓開發(fā)者可以快速的實(shí)現(xiàn)同步。它就像一個(gè)星星,遠(yuǎn)遠(yuǎn)看去就是一個(gè)小小的點(diǎn)。但是走近一看,卻是一個(gè)龐大的蛋糕。而這篇文章就是要將這個(gè)巨大的蛋糕切開,吃進(jìn)肚子里面去。

Synchronized 使用

在 Java 中,如果要實(shí)現(xiàn)同步,Java 提供了一個(gè)關(guān)鍵詞 synchronized 來讓開發(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 獲取對象 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í)行該方法之前,會自動獲取該對象鎖,獲取到對象鎖之后才會繼續(xù)執(zhí)行實(shí)例方法中的代碼;用于靜態(tài)方法上時(shí),線程執(zhí)行該方法之前,會自動獲取該對象所屬類的鎖,獲取到類鎖之后才會繼續(xù)執(zhí)行靜態(tài)方法中的代碼。用于代碼塊上時(shí),可以傳入任意對象作為鎖,并且可以控制鎖的粒度。

synchronized 實(shí)現(xiàn)原理

下面是 Test 類的字節(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
}

我們通過查看字節(jié)碼可以發(fā)現(xiàn),synchronized 關(guān)鍵字作用在實(shí)例方法和靜態(tài)方法上時(shí),JVM 是通過 ACC_SYNCHRONIZED 這個(gè)標(biāo)志來實(shí)現(xiàn)同步的。而作用在代碼塊時(shí),而且通過指令 monitorenter 和 monitorexit 來實(shí)現(xiàn)同步的。monitorenter 是獲取鎖的指令,monitorexit 則是釋放鎖的指令。

對象頭

通過上文我們已經(jīng)知道,Java 要實(shí)現(xiàn)同步,需要通過獲取對象鎖。那么在 JVM中,是如何知道哪個(gè)線程已經(jīng)獲取到了鎖呢?

要解釋這個(gè)問題,我們首先需要了解一個(gè)對象的存儲分布由以下三部分組成:

  • 對象頭(Header) :由 Mark WordKlass Pointer 組成
  • 實(shí)例數(shù)據(jù)(Instance Data) :對象的成員變量及數(shù)據(jù)
  • 對齊填充(Padding) :對齊填充的字節(jié)

Mark Word ****記錄了對象運(yùn)行時(shí)的數(shù)據(jù):

  • identity_hashcode:哈希碼,只要獲取了才會有
  • age:GC分代年齡
  • biased_lock: 1表示偏向鎖,0表示非偏向鎖
  • lock 鎖狀態(tài) :01 無鎖/偏向鎖;00 輕量級鎖;10 重量級鎖;11 GC 標(biāo)志
  • 偏向線程 ID
128bit (對象頭)狀態(tài)
64bit Mark Word64bit Klass Poiter
unused:25identity_hashcode:31unused:1age:4biased_lock:1lock:2無鎖
threadId:54epoch:2unused:1age:4biased_lock:1lock:2偏向鎖
ptr_to_lock_record:62lock:2輕量級鎖
ptr_to_heavyweight_monitor:62lock:2重量級鎖
lock:2GC 標(biāo)記

當(dāng)線程獲取對象鎖的時(shí)候,需要先通過對象頭中的 Mark Word 判斷對象鎖是否已經(jīng)被其他線程獲取,如果沒有,那么線程需要往對象頭中寫入一些標(biāo)記數(shù)據(jù),用于表示這個(gè)對象鎖已經(jīng)被我獲取了,其他線程無法再獲取到。如果對象鎖已經(jīng)被其他線程獲取了,那么線程就需要進(jìn)入到等待隊(duì)列中,直到持有鎖的線程釋放了鎖,它才有機(jī)會繼續(xù)獲取鎖。

當(dāng)一個(gè)線程擁有了鎖之后,它便可以多次進(jìn)入。當(dāng)然,在這個(gè)線程釋放鎖的時(shí)候,那么也需要執(zhí)行相同次數(shù)的釋放動作。比如,一個(gè)線程先后3次獲得了鎖,那么它也需要釋放3次,其他線程才可以繼續(xù)訪問。這也說明使用 synchronized 獲取的鎖,都是可重入鎖

字節(jié)序

我們知道了對象頭的內(nèi)存結(jié)構(gòu)之后,我們還需要了解一個(gè)很重要的概念:字節(jié)序。它表示每一個(gè)字節(jié)之間的數(shù)據(jù)在內(nèi)存中是如何存放的?如果不理解這個(gè)概念,那么在之后打印出對象頭時(shí),也會無法跟上述展示的對象頭內(nèi)存結(jié)構(gòu)相互對應(yīng)上。

字節(jié)序:大于一個(gè)字節(jié)的數(shù)據(jù)在內(nèi)存中的存放順序。

注意!注意!注意!這里使用了大于,也就是說一個(gè)字節(jié)內(nèi)的數(shù)據(jù),它的順序是固定的。

  • 大端序(BIG_ENDIAN):高位字節(jié)排在內(nèi)存的低地址處,低位字節(jié)排在內(nèi)存的高地址處。符合人類的讀寫順序
  • 小端序(LITTLE_ENDIAN):高位字節(jié)排在內(nèi)存的高地址處,低位字節(jié)排在內(nèi)存的低地址處。符合計(jì)算機(jī)的讀取順序

我們來舉個(gè)例子:

有一個(gè)十六進(jìn)制的數(shù)字:0x123456789。

使用大端序閱讀:高位字節(jié)在前,低位字節(jié)在后。

內(nèi)存地址12345
十六進(jìn)制0x010x230x450x670x89
二進(jìn)制0000000100100011010001010110011110001001

使用小端序閱讀:低位字節(jié)在前,高位字節(jié)在后。

內(nèi)存地址12345
十六進(jìn)制0x890x670x450x230x01
二進(jìn)制1000100101100111010001010010001100000001

既然大端序符合人類的閱讀習(xí)慣,那么統(tǒng)一使用大端序不就好了嗎?為什么還要搞出一個(gè)小端序來呢?

這是因?yàn)橛?jì)算機(jī)都是先從低位開始處理的,這樣處理效率比較高,所以計(jì)算機(jī)內(nèi)部都是使用小端序。其實(shí)計(jì)算機(jī)也不知道什么是大端序,什么是小端序,它只會按順序讀取字節(jié),先讀第一個(gè)字節(jié),再讀第二個(gè)字節(jié)。

Java 中的字節(jié)序

我們可以通過下面這一段代碼打印出 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é)序之后,我們來看看如何閱讀對象頭。

首先,我們使用一個(gè)第三方類庫 jol-core,我使用的是 0.10 版本,幫助我們打印出對象頭的數(shù)據(jù)。

我們可以通過下面這一段代碼打印出 Java 的對象頭:

public class ObjectHeaderPrinter {
    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        System.out.println("=====打印匿名偏向鎖對象頭=====");
        System.out.println(ClassLayout.parseInstance(test).toPrintable());
        synchronized (test){
            System.out.println("=====打印偏向鎖對象頭=====");
            System.out.println(ClassLayout.parseInstance(test).toPrintable());
        }
    }
}

打印結(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

=====打印偏向鎖對象頭=====
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

我們把對象頭的內(nèi)存結(jié)構(gòu)和對象頭單獨(dú)拿出來對照著解釋一下:

128bit (對象頭)狀態(tài)
64bit Mark Word64bit Klass Poiter
unused:25identity_hashcode:31unused:1age:4biased_lock:1lock:2匿名偏向鎖/無鎖
threadId:54epoch:2unused:1age:4biased_lock:1lock:2偏向鎖
ptr_to_lock_record:62lock:2輕量級鎖
ptr_to_heavyweight_monitor:62lock:2重量級鎖
lock:2GC 標(biāo)記
// 匿名偏向鎖/無鎖
// 我們給每個(gè)字節(jié)都標(biā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:25identity_hashcode:31unused:1age:4biased_lock:1lock:2
hgf + e的最高位e 的低 7 位 + dcba 的最高位a的第 4-7 位a的第 3 位a的低 2 位
00000000 00000000 00000000 00000000 00000000 00000000 0000000000000101

我們再來看一個(gè)加了偏向鎖的對象頭:

// 偏向鎖
                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:54epoch:2unused:1age:4biased_lock:1lock:2
hgfedc + b 的高 6 位b的低 2 位a 的最高位a的第 4-7 位a的第 3 位a的低 2 位
00000000 00000000 00000000 00000001 00010011 00000000 1001000000000101

偏向鎖

偏向鎖是 Java 為了提高獲取鎖的效率和降低獲取鎖的代價(jià),而進(jìn)行的一個(gè)優(yōu)化。因?yàn)?Java 團(tuán)隊(duì)發(fā)現(xiàn)大多數(shù)的鎖都只被一個(gè)線程獲取。基于這種情況,就可以認(rèn)為鎖都只被一個(gè)線程獲取,那么就不會存在多個(gè)線程競爭的條件,因此就可以不需要真正的去獲取一個(gè)完整的鎖。只需要在對象頭中寫入獲取鎖的線程 ID,用于表示該對象鎖已經(jīng)被該線程獲取。

獲取偏向鎖,只要修改對象頭的標(biāo)記就可以表示線程已經(jīng)獲取了鎖,大大降低了獲取鎖的代價(jià)。

當(dāng)線程獲取對象的偏向鎖時(shí),它的對象頭:

threadId:54epoch:2unused:1age:4biased_lock:1lock:2

threadId:獲取了偏向鎖的線程 ID

epoch:用于保存偏向時(shí)間戳

age:對象 GC 年齡

biased_lock:偏向鎖標(biāo)記,此時(shí)為 1

lock:鎖標(biāo)記,此時(shí)為 10

獲取偏向鎖

線程獲取對象鎖時(shí),首先檢查對象鎖是否支持偏向鎖,即檢查 biased_lock 是否為 1;如果為 1,那么將會檢查threadId 是否為 null,如果為 null,將會通過 CAS 操作將自己的線程 ID 寫入到對象頭中。如果成功寫入了線程 ID,那么該線程就獲取到了對象的偏向鎖,可以繼續(xù)執(zhí)行后面的同步代碼。

只有匿名偏向的對象才能進(jìn)入偏向鎖模式,即該對象還沒有偏向任何一個(gè)線程(不是絕對的,存在批量重偏向的情況)。

釋放偏向鎖

線程是不會主動釋放偏向鎖的。只有當(dāng)其它線程嘗試競爭偏向鎖時(shí),持有偏向鎖的線程才會釋放偏向鎖。

釋放偏向鎖需要在全局安全點(diǎn)進(jìn)行。釋放的步驟如下:

  • 暫停擁有偏向鎖的線程,判斷是否處于同步代碼塊中,如果處于,則進(jìn)行偏向撤銷,并升級為輕量級鎖。
  • 如果不處于,則恢復(fù)為無鎖狀態(tài)。

由此可以知道,偏向鎖天然是可重入的。

偏向撤銷

偏向撤銷主要發(fā)生在多個(gè)線程存在競爭,不再偏向于任何一個(gè)線程了。也就是說偏向撤銷之后,將不會再使用偏向鎖。具體操作就是將 Mark Work 中的 biased_lock 由 1 設(shè)置為 0 。 偏向撤銷需要到達(dá)全局安全點(diǎn)才可以撤銷,因?yàn)樗枰薷膶ο箢^,并從棧中獲取數(shù)據(jù)。因此偏向撤銷也會存在較大的資源消耗。

想要撤銷偏向鎖,還不能對持有偏向鎖的線程有影響,所以就要等待持有偏向鎖的線程到達(dá)一個(gè) safepoint 安全點(diǎn),在這個(gè)安全點(diǎn)會掛起獲得偏向鎖的線程。

  • 如果原持有偏向鎖的線程依然還在同步代碼塊中,那么就會將偏向鎖升級為輕量級鎖。
  • 如果原持有偏向鎖的線程已經(jīng)死亡,或者已經(jīng)退出了同步代碼塊,那么直接撤銷偏向鎖狀態(tài)即可。

對象的偏向鎖被撤銷之后,對象在未來將不會偏向于任何一個(gè)線程。

批量重偏向

我們可以想象,如果有 100 個(gè)對象都偏向于一個(gè)線程,此時(shí)如果有另外一個(gè)線程來獲取這些對象的鎖,那么這 100 個(gè)對象都會發(fā)生偏向撤銷,而這 100 次偏向撤銷都需要在全局安全點(diǎn)下進(jìn)行,這樣就會產(chǎn)生大量的性能消耗。

批量重偏向就是建立在撤銷偏向會對性能產(chǎn)生較大影響情況下的一種優(yōu)化措施。當(dāng) JVM 知道有大量對象的偏向鎖撤銷時(shí),它就知道此時(shí)這些對象都不會偏向于原線程,所以會將對象重新偏向于新的線程,從而減少偏向撤銷的次數(shù)。

當(dāng)一個(gè)類的大量對象被同一個(gè)線程 T1 獲取了偏向鎖,也就是大量對象先偏向于該線程 T1。T1 同步結(jié)束后,另一個(gè)線程 T2 對這些同一類型的對象進(jìn)行同步操作,就會讓這些對象重新偏向于線程 T2。

在了解批量重偏向前,我們需要先了解一點(diǎn)其他知識:

JVM 會給對象的類對象 class 賦予兩個(gè)屬性,一個(gè)是偏向撤銷計(jì)數(shù)器,一個(gè)是 epoch 值。

我們先來看一個(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)入偏向模式,可以通過參數(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)建完對象后,保持線程t1狀態(tài)為存活
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();
        //睡眠3s鐘保證線程t1創(chuàng)建對象完成
        Thread.sleep(3000);
        System.out.println("打印t1線程,list中第20個(gè)對象的對象頭:");
        System.out.println((ClassLayout.parseInstance(listA.get(19)).toPrintable()));
        //創(chuàng)建線程t2競爭線程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) {
                        // 該對象已經(jīng)是輕量級鎖,無法降級,因此只能是輕量級鎖
                        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è)對象的對象頭:");
        System.out.println((ClassLayout.parseInstance(listA.get(10)).toPrintable()));
        System.out.println("打印list中第26個(gè)對象的對象頭:");
        System.out.println((ClassLayout.parseInstance(listA.get(25)).toPrintable()));
        System.out.println("打印list中第41個(gè)對象的對象頭:");
        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è)對象都會偏向于 t1。
  • t2 執(zhí)行完畢之后,其中前 19 個(gè)對象都會撤銷偏向鎖,此時(shí)類中的偏向撤銷計(jì)數(shù)器為19。但當(dāng)撤銷到第 20 個(gè)的時(shí)候,偏向撤銷計(jì)數(shù)器為 20,此時(shí)達(dá)到 -XX:BiasedLockingBulkRebiasThreshold=20 的條件,于是將類中的 epoch 值 +1,并在此時(shí)找到所有處于同步代碼塊的對象,并將其 epoch 值等于類對象的 epoch 值。然后進(jìn)行批量重偏向操作,從第 20 個(gè)對象開始,將會比較對象的 epoch 值是否等于類對象的 epoch 值,如果不等于,那么直接使用 CAS 替換掉 Mark Word 中的程 ID 為當(dāng)前線程的 ID。

結(jié)論:

  • 前 19 個(gè)對象撤銷了偏向鎖,即 Mark Word 中的 biased_lock 為 0,如果有線程來獲取鎖,那么先獲取輕量級鎖。
  • 第 20 - 30 個(gè)對象,依然為偏向鎖,偏向于線程 t2。
  • 第 31 - 100 個(gè)對象,依然為偏向鎖,偏向于線程 t1。

tech.youzan.com/javasuo-yu-…

暫時(shí)無法在飛書文檔外展示此內(nèi)容

批量撤銷偏向

當(dāng)偏向鎖撤銷的數(shù)量達(dá)到 40 時(shí),就會發(fā)生批量撤銷。但是,這是在一個(gè)時(shí)間范圍內(nèi)達(dá)到 40 才會發(fā)生,這個(gè)時(shí)間范圍通過 -XX:BiasedLockingDecayTime設(shè)置,默認(rèn)值為 25 秒。

也就是在發(fā)生批量偏向的 25 秒內(nèi),如果偏向鎖撤銷的數(shù)量達(dá)到了 40 ,那么就會發(fā)生批量撤銷,將該類下的所有對象都進(jìn)行撤銷偏向,包括后續(xù)創(chuàng)建的對象。如果在發(fā)生批量偏向的 25 秒內(nèi)沒有達(dá)到 40 ,就會重置偏向鎖撤銷數(shù)量,將偏向鎖撤銷數(shù)量重置為 20。

Hashcode 去哪了

我們通過 Mark Word 知道,在無鎖狀態(tài)下,如果調(diào)用對象的 hashcode() 方法,就會在 Mark Word 中記錄對象的 Hashcode 值,在下一次調(diào)用 hashcode() 方法時(shí),就可以直接通過 Mark Word 來得知,而不需要再次計(jì)算,以此來保證 Hashcode 的一致性。

但是獲取了鎖之后,就會修改 Mark Word 中的值,那么之前記錄下來的 Hashcode 值去哪里了呢?

Lock Record

在解答這個(gè)問題之前,我們需要先知道一個(gè)東西:Lock Record。

當(dāng)字節(jié)碼解釋器執(zhí)行 monitorenter 字節(jié)碼輕度鎖住一個(gè)對象時(shí),就會在獲取鎖的線程棧上顯式或者隱式分配一個(gè) Lock Record。換句話說,就是在獲取輕量級鎖時(shí),會在線程棧上分配一個(gè) Lock Record。這個(gè) Lock Record 說直白一點(diǎn)就是棧上的一塊空間,主要用于存儲相關(guān)信息。

Lock Record 只要有三個(gè)作用:

  • 持有 Displaced Word(就是對象的 Mark Word)和一些元信息用于識別哪個(gè)對象被鎖住了。
  • 解釋器使用 Lock Record 來檢測非法的鎖狀態(tài)
  • 隱式地充當(dāng)鎖重入機(jī)制的計(jì)數(shù)器

那么這個(gè) Lock Record 跟 Hashcode 有什么關(guān)系呢?

場景 1

我們先來看第一個(gè)場景:先獲取對象的 hashcode,然后再獲取對象的鎖。

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

  • 步驟一:未獲取對象的 hashcode 值之前,對象處于匿名偏向鎖狀態(tài)。鎖標(biāo)記為:101
  • 步驟二:獲取對象的 hashcode 之后,對象的偏向狀態(tài)被撤銷,處于無鎖狀態(tài)。鎖標(biāo)記為:001。對象頭中也存儲了 hashcode 值,hashcode 值為 0111011 10001011 10010111 00001100。
  • 步驟三:獲取鎖之后,對象處于輕量級鎖狀態(tài)。鎖標(biāo)記為:00。其余 62 位為指向 Lock Record 的指針。從這里我們可以看到,Mark Word 中已經(jīng)沒有 hashcode 了。整塊 Mark Word 的內(nèi)容已經(jīng)被復(fù)制到 Lock Word 中。
  • 步驟四:釋放鎖之后,對象處于無鎖狀態(tài)。鎖標(biāo)記為:001。在 Mark Word 中也可以看到之前生成的 hashcode。與步驟二中的 Mark Word 一模一樣。這是因?yàn)樵卺尫沛i之后,JVM 會將 Lock Record 中的值復(fù)制回 Mark Word 中,并刪除 Lock Record。

結(jié)論:

  • 當(dāng)對象生成 hashcode 之后,會撤銷偏向,并將 hashcode 記錄在 Mark Word 中。
  • 非偏向的對象獲取鎖時(shí),會先在棧中生成一個(gè) Lock Record。并將對象的 Mark Word 復(fù)制到 Lock Record 中。

場景2

我們現(xiàn)在來看第二個(gè)場景:先獲取對象的鎖,然后在同步代碼塊中生成 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

  • 步驟一:未獲取對象的 hashcode 值之前,對象處于匿名偏向鎖狀態(tài)。鎖標(biāo)記為:101
  • 步驟二:進(jìn)入同步代碼塊,線程獲取了偏向鎖。鎖標(biāo)記:101
  • 步驟三:對象生成 hashcode,此時(shí)鎖標(biāo)記:10,直接從偏向鎖升級為重量級鎖。 其余 62 位為指向 objectMonitor 的指針。

與輕量級鎖存在同樣的問題,hashcode 會存放在哪里?每一個(gè)對象在 JVM 中都有一個(gè) objectMonitor 對象,而 Mark Word 就存儲在 objectMonitor 對象的 header 屬性中。

輕量級鎖

輕量級鎖解決的場景是:任意兩個(gè)線程交替獲取鎖的情況。主要依靠 CAS 操作,相比較于使用重量級鎖,可以減少鎖資源的消耗。

獲取輕量級鎖

使用輕量級鎖的情況有以下幾種:

  • 禁用偏向鎖。
  • 偏向鎖失效,升級為輕量級鎖。

禁用偏向鎖導(dǎo)致升級

在啟動 Java 程序時(shí),如果添加了 JVM 參數(shù) -XX:-UseBiasedLocking , 那么在后續(xù)的運(yùn)行中,就不再使用偏向鎖 。

偏向鎖失效,升級為輕量級鎖

如果對象發(fā)生偏向撤銷時(shí):

  • 首先會檢查持有偏向鎖的線程是否已經(jīng)死亡,如果死亡,則直接升級為輕量級鎖,否則,執(zhí)行步驟2
  • 查看持有偏向鎖的線程是否在同步代碼塊中,如果在,則將偏向鎖升級為輕量級鎖,否則,執(zhí)行步驟3
  • 修改 Mark Word 為非偏向模式,設(shè)置為無鎖狀態(tài)。

加鎖過程

當(dāng)線程獲取輕量級鎖時(shí),首先會在線程棧中創(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í)無法在飛書文檔外展示此內(nèi)容

當(dāng)數(shù)據(jù)復(fù)制成功之后,JVM 將會使用 CAS 嘗試修改 Mark Word 中的數(shù)據(jù)為指向線程棧中 Displated Mark Word 的指針,并將 Lock Record 中的 owner 指針指向 Mark Word。

如果這兩步操作都更新成功了,那么則表示該線程獲得輕量級鎖成功,設(shè)置 Mark Word 中的 lock 字段為 00,表示當(dāng)前對象為輕量級鎖狀態(tài)。同步,線程可以執(zhí)行同步代碼塊。

如果更新操作失敗了,那么 JVM 將會檢查 Mark Word 是否指向當(dāng)前線程的棧幀:

  • 如果是,則表示當(dāng)前線程已經(jīng)獲取了輕量級鎖,會在棧幀中添加一個(gè)新的 Lock Record,這個(gè)新 Lock Record 中的 Displated Mark Word 為 null,owner 指向?qū)ο蟆_@樣的目的是為了統(tǒng)計(jì)重入的鎖數(shù)量,因此,在棧中會有一個(gè) Lock Record 的列表。完成這一步之后就可以直接執(zhí)行同步代碼塊。

暫時(shí)無法在飛書文檔外展示此內(nèi)容

  • 如果不是,那么表示輕量級鎖發(fā)生競爭,后續(xù)將會膨脹為重量級鎖。

釋放輕量級鎖

釋放輕量級鎖時(shí),會在棧中由低到高,獲取 Lock Record。查詢到 Lock Record 中的 Displated Mark Word 為 null 時(shí),則表示,該鎖是重入的,只需要將 owner 設(shè)置為 null 即可,表示已經(jīng)釋放了這個(gè)鎖。如果 Displated Mark Word 不為 null,則需要通過 CAS 將 Displated Mark Word 拷貝至對象頭的 Mark Word 中,然后將 owner 的指針設(shè)置為 null,最后修改 Mark Word 的 lock 字段為 01 無鎖狀態(tài)。

重量級鎖

重量級鎖解鎖的場景是:多個(gè)線程相互競爭同一個(gè)鎖。主要通過 park()unpark()方法,結(jié)合隊(duì)列來完成。相較于輕量級鎖和偏向鎖,需要切換內(nèi)核態(tài)和用戶態(tài)環(huán)境,因此獲取鎖的過程會消耗較多的資源。

獲取重量級鎖

使用重量級鎖的情況有兩種:

  • 在持有偏向鎖的情況下,直接獲取對象的 hashcode,將會直接升級為重量級鎖。
  • 在輕量級鎖的情況下,存在競爭,膨脹為重量級鎖。

獲取 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());
        }
    }
}

執(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(),會發(fā)現(xiàn)偏向鎖直接膨脹為重量級鎖了。我們可以看到 lock 字段為 10。

這里有一個(gè)疑問,為什么不是升級為輕量級鎖呢?輕量級鎖也可以在 Lock Record 中存儲生成的 hashcode。而膨脹為更為消耗資源的重量級鎖。

輕量級鎖膨脹為重量級鎖

當(dāng)處于輕量級鎖的時(shí)候,說明鎖已經(jīng)不再偏向于任何一個(gè)線程,但是也沒有發(fā)生競爭,可以依靠 CAS 獲取到輕量級鎖。但是當(dāng)出現(xiàn) CAS 獲取鎖失敗時(shí),就會直接膨脹為重量級鎖。

這里需要注意,只會 CAS 一次,只要一次失敗就會直接膨脹為重量級鎖,而不是達(dá)到自旋次數(shù)或者自旋時(shí)間才膨脹。

膨脹過程

在膨脹過程中,會有幾種標(biāo)記來表示鎖的狀態(tài):

  • Inflated:膨脹已完成
  • Stack-locked:輕量級鎖
  • INFLATING:膨脹中
  • Neutral:無鎖

膨脹步驟:

檢查是否已經(jīng)為重量級鎖,如果是直接返回。

檢查是否處于膨脹中的狀態(tài),如果是,循環(huán)檢測狀態(tài)。檢測出膨脹中的狀態(tài)是因?yàn)橛衅渌€程正在進(jìn)行膨脹,因?yàn)樾枰却蛎浲瓿芍?,才能繼續(xù)執(zhí)行。

檢查是否為輕量級鎖,如果是,則執(zhí)行以下步驟:

  • 創(chuàng)建一個(gè) ObjectMonitor 對象。
  • 通過 CAS 設(shè)置 Mark Word 為全 0,用以表示 INFLATING 狀態(tài)。如果失敗,則從步驟 1 重新開始執(zhí)行。
  • 將 Mark Word 設(shè)置到 ObjectMonitor 對象中。
  • 設(shè)置 owner 屬性為 Lock Record
  • 設(shè)置 Mark Word 值

返回

  • 判定為無鎖狀態(tài),執(zhí)行以下步驟:
  • 創(chuàng)建一個(gè) ObjectMonitor 對象。
  • 通過 CAS 直接設(shè)置 Mark Word 值。
  • 返回

競爭鎖過程

我們要理解如何獲取重量級鎖,需要先了解 ObjectMonitor 對象。顧名思義,這是一個(gè)對象監(jiān)視器。在 Java 中,每個(gè)對象都有一個(gè)與之對應(yīng)的 ObjectMonitor 。ObjectMonitor 內(nèi)部有幾個(gè)重要的字段:

  • cxq:存放被阻塞的線程
  • EntryList:存放被阻塞的線程,在釋放鎖時(shí)使用
  • WaitSet:獲得鎖的線程,如果調(diào)用 wait() 方法,那么線程會被存放在此處,這是一個(gè)雙向循環(huán)鏈表
  • onwer:持有鎖的線程

cxq,EntryList 均為 ObjectWaiter 類型的單鏈表。

獲取鎖過程

  • 通過 CAS 設(shè)置 onwer 為當(dāng)前線程(嘗試獲取鎖),CAS 的原值為 NULL,新值為 current_thread,如果成功,則表示獲得鎖。否則執(zhí)行步驟 2
  • 判斷當(dāng)前線程與獲取鎖線程是否一致,如果一致,則表示獲得鎖(鎖重入)。否則執(zhí)行步驟 3
  • 判斷當(dāng)前線程是否為之前持有輕量級鎖的線程,如果是,直接設(shè)置 onwer 為當(dāng)前線程,表示獲得鎖。否則執(zhí)行步驟 4

以上步驟都失敗,則嘗試一輪自旋來獲取鎖。如果未獲取鎖,則執(zhí)行步驟 5

使用阻塞和喚醒來控制線程競爭鎖

通過 CAS 設(shè)置 owner 為當(dāng)前線程(嘗試獲取鎖),CAS 的原值為 NULL,新值為 current_thread。如果成功,則表示獲得鎖。否則執(zhí)行步驟 b

通過 CAS 設(shè)置 owner 為當(dāng)前線程(嘗試獲取鎖)CAS 的原值為 DEFLATER_MARKER,新值為 current_thread。如果成功,則表示獲得鎖。否則執(zhí)行步驟c。(DEFLATER_MARKER 是一個(gè)鎖降級的標(biāo)記,后續(xù)會講解。)

以上步驟都失敗,則嘗試一輪自旋來獲取鎖。如果未獲取鎖,則執(zhí)行步驟 d。

為當(dāng)前線程創(chuàng)建一個(gè) ObjectWaiter 類型的 node 節(jié)點(diǎn)。步驟 i 和 ii 是一個(gè)循環(huán),直到一個(gè)成功才會跳出這個(gè)循環(huán)。

通過 cas 插入 cxq 的頭部,如果插入失敗,則執(zhí)行步驟 ii

通過 CAS 設(shè)置 owner 為當(dāng)前線程(嘗試獲取鎖),CAS 的原值為 NULL,新值為 current_thread。如果失敗,則執(zhí)行 i。

通過 CAS 設(shè)置 owner 為當(dāng)前線程(嘗試獲取鎖),CAS 的原值為 NULL,新值為 current_thread。如果成功,則表示獲得鎖。否則執(zhí)行步驟 f。(該步驟往下開始是一個(gè)循環(huán),直到獲取到鎖為止)

通過 park(),將線程阻塞。

線程被喚醒后

通過 CAS 設(shè)置 owner 為當(dāng)前線程(嘗試獲取鎖),CAS 的原值為 NULL,新值為 current_thread。如果成功,則表示獲得鎖。否則執(zhí)行步驟 ii

通過 CAS 設(shè)置 owner 為當(dāng)前線程(嘗試獲取鎖)CAS 的原值為 DEFLATER_MARKER,新值為 current_thread。如果成功,則表示獲得鎖。否則執(zhí)行 iii

嘗試一輪自旋來獲取鎖。如果未獲取鎖,則跳轉(zhuǎn)回步驟 e 執(zhí)行。

自適應(yīng)自旋鎖主要是用于重量級鎖中,降低阻塞線程概率。而不是用于輕量級鎖,這里大家要多多注意。

釋放重量級鎖

釋放鎖過程

判斷 _owner 字段是否等于 current_thread。如果等于則判斷當(dāng)前線程是否為持有輕量級鎖的線程,如果是的話,表示該線程還沒有執(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 中,涉及到鏈表的移動,如果多線程執(zhí)行,將會出錯(cuò)。)

如何 _EntryList 非空,那么取 _EntryList 中的第一個(gè)元素,將該元素下的線程喚醒。否則執(zhí)行步驟 b。

將 _cxq 設(shè)置為空,并將 _cxq 的元素按照原順序放入 _EntryList 中。然后取 _EntryList 中的第一個(gè)元素,將該元素下的線程喚醒。

線程喚醒

設(shè)置 _owner 字段為 NULL,解鎖成功,讓后續(xù)線程可以正常獲取到鎖。

然后調(diào)用 unpark() 方法,喚醒線程。

wait(),notify(),notifyAll()

我們需要知道一個(gè)前提,在處理 wait 方法時(shí),必須使用重量級鎖。因此,wait 方法會導(dǎo)致鎖升級。

我們先來看一個(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 首先獲取到鎖,然后通過 wait() 方法,將鎖釋放,并且等待通知。
  • 睡眠 1 S,這里是確保線程 A 可以順利完成所有操作。
  • 因?yàn)?A 釋放了鎖,所以線程 B 可以獲取到鎖。然后創(chuàng)建了線程 C。
  • 因?yàn)榫€程 B 睡眠了 2S,依然持有鎖,所以線程 C 無法獲取到鎖,只能繼續(xù)等待。
  • 線程 B 調(diào)用 notify() 方法,線程 A 被喚醒,開始競爭鎖。
  • 線程 A 和線程 C 競爭鎖。

但是根據(jù)打印結(jié)果,無論執(zhí)行多少次,都是線程 A 先獲取鎖。

第一個(gè)問題:為什么都是線程 A 先獲取鎖,而不是線程 C 先獲取鎖?

第二個(gè)問題:為什么 wait 方法并沒有生成 monitorenter 指令,也可以獲取到鎖?

第三個(gè)問題:執(zhí)行 wait 之后,線程去哪里了?它的狀態(tài)是什么?

為了解答這些問題,我們需要深入到源碼中去。但是這里就不放源碼了,我只講一下關(guān)鍵步驟:

wait()

  • 膨脹為重量級鎖
  • 為 current_thread 創(chuàng)建 ObjectWaiter 類型的 node 節(jié)點(diǎn)
  • 將 node 放入 _waitSet 中
  • 釋放鎖
  • 通過 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)檢測 _waitSet 是否不為空
  • 如果不為空,則執(zhí)行 notify() 的步驟。
  • 否則返回

第一個(gè)問題:執(zhí)行 wait 之后,線程去哪里了?它的狀態(tài)是什么?

線程 A 調(diào)用 wait() 方法后,線程 A 就被 park 了,并被放入到 _waitSet 中。此時(shí)他的狀態(tài)就是 WAITING。如果它從 _waitSet 移除,并被放入到 cxq 之后,那么他的狀態(tài)就會變?yōu)?BLOCKED。如果它競爭到鎖,那么他的狀態(tài)就會變?yōu)?RUNNABLE 。

第二個(gè)問題:為什么 wait 方法并沒有生成 monitorenter 指令,也可以獲取到鎖?

線程 A 調(diào)用 wait() 方法后,線程 A 被放入到 _waitSet 中。直到有其他線程調(diào)用 notify() 之后,線程 A 從 _waitSet 移除,并放入到 cxq 中。

第三個(gè)問題:為什么都是線程 A 先獲取鎖,而不是線程 C 先獲取鎖?

線程 A 調(diào)用 wait() 方法后,線程 A 被放入到 _waitSet 中。線程 B 獲取鎖,然后創(chuàng)建了線程 C,線程 C 競爭鎖失敗,被放入到 cxq 中。然后 B 調(diào)用 notify() 方法后,線程 A 從 _waitSet 移除,放入到 cxq 的頭部。因此目前 cxq 的鏈表結(jié)構(gòu)為:A -> C -> null。接著線程 B 釋放鎖,會將 cxq 中的元素按照原順序放入到 EntryList 中,因此目前 cxq 鏈表結(jié)構(gòu)為:null;EntryList 鏈表結(jié)構(gòu)為:A -> C -> null。然后喚醒 EntryList 中的第一個(gè)線程。

所以,每次都是線程 A 先獲取鎖。

以上就是從 JVM 中深入探究 Synchronized作用及原理的詳細(xì)內(nèi)容,更多關(guān)于JVM探究Synchronized的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • gson ajax 數(shù)字精度丟失問題的解決方法

    gson ajax 數(shù)字精度丟失問題的解決方法

    下面小編就為大家?guī)硪黄猤son ajax 數(shù)字精度丟失問題的解決方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-03-03
  • java數(shù)組元素的引用實(shí)例講解

    java數(shù)組元素的引用實(shí)例講解

    在本篇文章里小編給大家整理的是一篇關(guān)于java數(shù)組元素的引用實(shí)例講解內(nèi)容,有需要的朋友們可以學(xué)習(xí)參考下。
    2021-03-03
  • Mybatis如何自動生成sql語句

    Mybatis如何自動生成sql語句

    這篇文章主要介紹了Mybatis如何自動生成sql語句,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-12-12
  • SpringBoot讀取配置文件常用方法解析

    SpringBoot讀取配置文件常用方法解析

    這篇文章主要介紹了SpringBoot讀取配置文件常用方法解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-07-07
  • 生產(chǎn)消費(fèi)者模式實(shí)現(xiàn)方式和線程安全問題代碼示例

    生產(chǎn)消費(fèi)者模式實(shí)現(xiàn)方式和線程安全問題代碼示例

    這篇文章主要介紹了生產(chǎn)消費(fèi)者模式實(shí)現(xiàn)方式和線程安全問題代碼示例,具有一定借鑒價(jià)值,需要的朋友可以參考下
    2017-12-12
  • Gradle:修改默認(rèn)的Build配置文件名方式

    Gradle:修改默認(rèn)的Build配置文件名方式

    這篇文章主要介紹了Gradle:修改默認(rèn)的Build配置文件名方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-03-03
  • Java Lambda表達(dá)式與引用類淺析

    Java Lambda表達(dá)式與引用類淺析

    Lambda表達(dá)式是Java SE8中一個(gè)重要的新特性,允許通過表達(dá)式來代替功能接口。本文將通過一些簡單的示例和大家講講Lamda表達(dá)式的使用,感興趣的可以了解一下
    2023-01-01
  • 教你怎么用SpringBoot整合Swagger作為API

    教你怎么用SpringBoot整合Swagger作為API

    這篇文章主要介紹了教你怎么用SpringBoot整合Swagger作為API,文中有非常詳細(xì)的代碼示例,對正在學(xué)習(xí)java的小伙伴們有很好的幫助,需要的朋友可以參考下
    2021-05-05
  • Java基于IDEA實(shí)現(xiàn)http編程的示例代碼

    Java基于IDEA實(shí)現(xiàn)http編程的示例代碼

    這篇文章主要介紹了Java基于IDEA實(shí)現(xiàn)http編程的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-04-04
  • 開源的Java圖片處理庫實(shí)例詳解

    開源的Java圖片處理庫實(shí)例詳解

    Java?圖片處理庫提供了豐富的功能,用于處理和增強(qiáng)圖像,在Java生態(tài)系統(tǒng)中,有幾個(gè)流行的開源庫可以用于圖片處理,這些庫提供了豐富的功能,如圖像縮放、裁剪、顏色調(diào)整、格式轉(zhuǎn)換等,本文介紹開源的Java圖片處理庫介紹,感興趣的朋友一起看看吧
    2024-03-03

最新評論