溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

JVM中Synchronized作用及原理是什么

發(fā)布時(shí)間:2023-03-30 15:20:33 來(lái)源:億速云 閱讀:89 作者:iii 欄目:開(kāi)發(fā)技術(shù)

這篇文章主要介紹“JVM中Synchronized作用及原理是什么”,在日常操作中,相信很多人在JVM中Synchronized作用及原理是什么問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”JVM中Synchronized作用及原理是什么”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!

Synchronized 使用

在 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ì)象作為鎖,并且可以控制鎖的粒度。

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

下面是 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 則是釋放鎖的指令。

對(duì)象頭

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

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

  • 對(duì)象頭(Header) :由 Mark WordKlass 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 Word64bit Klass Poiter





unused:25identity_hashcode:31unused:1age:4biased_lock:1lock:2
無(wú)鎖
threadId:54epoch:2unused:1age:4biased_lock:1lock:2
偏向鎖
ptr_to_lock_record:62lock:2
輕量級(jí)鎖



ptr_to_heavyweight_monitor:62lock: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 獲取的鎖,都是可重入鎖

字節(jié)序

我們知道了對(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)存地址12345
十六進(jìn)制0x010x230x450x670x89
二進(jìn)制0000000100100011010001010110011110001001

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

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

既然大端序符合人類(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é)。

Java 中的字節(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é)序。

如何閱讀對(duì)象頭

在理解了字節(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 Word64bit Klass Poiter





unused:25identity_hashcode:31unused:1age:4biased_lock:1lock:2
匿名偏向鎖/無(wú)鎖
threadId:54epoch:2unused:1age:4biased_lock:1lock:2
偏向鎖
ptr_to_lock_record:62lock:2
輕量級(jí)鎖



ptr_to_heavyweight_monitor:62lock: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: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

我們?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: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è)線程獲取,那么就不會(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:54epoch:2unused:1age:4biased_lock:1lock: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)

偏向撤銷(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-&hellip;

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

批量撤銷(xiāo)偏向

當(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。

Hashcode 去哪了

我們通過(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 值去哪里了呢?

Lock Record

在解答這個(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)系呢?

場(chǎng)景 1

我們先來(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 中。

場(chǎng)景2

我們現(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í)鎖

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

獲取輕量級(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)。

加鎖過(guò)程

當(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í)鎖

釋放輕量級(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í)鎖

重量級(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í)鎖

使用重量級(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ò)程

在膨脹過(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ìng)爭(zhēng)鎖過(guò)程

我們要理解如何獲取重量級(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í)鎖,這里大家要多多注意。

釋放重量級(jí)鎖

釋放鎖過(guò)程

判斷 _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() 方法,喚醒線程。

wait(),notify(),notifyAll()

我們需要知道一個(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í)用的文章!

向AI問(wèn)一下細(xì)節(jié)

免責(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)容。

AI