中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

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

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

JVM中Synchronized作用及原理是什么

發布時間:2023-03-30 15:20:33 來源:億速云 閱讀:89 作者:iii 欄目:開發技術

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

Synchronized 使用

在 Java 中,如果要實現同步,Java 提供了一個關鍵詞 synchronized 來讓開發人員可以快速實現同步代碼塊。

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 "靜態方法";
    }
    public void put(){
        synchronized (this){
            System.out.println("同步代碼塊");
        }
    }
}

synchronized 關鍵字除了可以用于代碼塊,還可以用于方法上。用于實例方法上時,線程執行該方法之前,會自動獲取該對象鎖,獲取到對象鎖之后才會繼續執行實例方法中的代碼;用于靜態方法上時,線程執行該方法之前,會自動獲取該對象所屬類的鎖,獲取到類鎖之后才會繼續執行靜態方法中的代碼。用于代碼塊上時,可以傳入任意對象作為鎖,并且可以控制鎖的粒度。

synchronized 實現原理

下面是 Test 類的字節碼文件

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            // 靜態方法
   #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               靜態方法
  #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 靜態方法
         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
}

我們通過查看字節碼可以發現,synchronized 關鍵字作用在實例方法和靜態方法上時,JVM 是通過 ACC_SYNCHRONIZED 這個標志來實現同步的。而作用在代碼塊時,而且通過指令 monitorenter 和 monitorexit 來實現同步的。monitorenter 是獲取鎖的指令,monitorexit 則是釋放鎖的指令。

對象頭

通過上文我們已經知道,Java 要實現同步,需要通過獲取對象鎖。那么在 JVM中,是如何知道哪個線程已經獲取到了鎖呢?

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

  • 對象頭(Header) :由 Mark WordKlass Pointer 組成

  • 實例數據(Instance Data) :對象的成員變量及數據

  • 對齊填充(Padding) :對齊填充的字節

Mark Word ****記錄了對象運行時的數據:

  • identity_hashcode:哈希碼,只要獲取了才會有

  • age:GC分代年齡

  • biased_lock: 1表示偏向鎖,0表示非偏向鎖

  • lock 鎖狀態 :01 無鎖/偏向鎖;00 輕量級鎖;10 重量級鎖;11 GC 標志

  • 偏向線程 ID

128bit (對象頭)狀態





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:2
GC 標記



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

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

字節序

我們知道了對象頭的內存結構之后,我們還需要了解一個很重要的概念:字節序。它表示每一個字節之間的數據在內存中是如何存放的?如果不理解這個概念,那么在之后打印出對象頭時,也會無法跟上述展示的對象頭內存結構相互對應上。

字節序:大于一個字節的數據在內存中的存放順序。

注意!注意!注意!這里使用了大于,也就是說一個字節內的數據,它的順序是固定的。

  • 大端序(BIG_ENDIAN):高位字節排在內存的低地址處,低位字節排在內存的高地址處。符合人類的讀寫順序

  • 小端序(LITTLE_ENDIAN):高位字節排在內存的高地址處,低位字節排在內存的低地址處。符合計算機的讀取順序

我們來舉個例子:

有一個十六進制的數字:0x123456789。

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

內存地址12345
十六進制0x010x230x450x670x89
二進制0000000100100011010001010110011110001001

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

內存地址12345
十六進制0x890x670x450x230x01
二進制1000100101100111010001010010001100000001

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

這是因為計算機都是先從低位開始處理的,這樣處理效率比較高,所以計算機內部都是使用小端序。其實計算機也不知道什么是大端序,什么是小端序,它只會按順序讀取字節,先讀第一個字節,再讀第二個字節。

Java 中的字節序

我們可以通過下面這一段代碼打印出 Java 的字節序:

public class ByteOrderPrinter {
    public static void main(String[] args){
        System.out.println(ByteOrder.nativeOrder());
    }
}

打印的結果為: LITTLE_ENDIAN。

因此,我們可以知道 Java 中的字節序為小端字節序。

如何閱讀對象頭

在理解了字節序之后,我們來看看如何閱讀對象頭。

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

我們可以通過下面這一段代碼打印出 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());
        }
    }
}

打印結果如下:

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

我們把對象頭的內存結構和對象頭單獨拿出來對照著解釋一下:

128bit (對象頭)狀態





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:2
GC 標記



// 匿名偏向鎖/無鎖
// 我們給每個字節都標上序號。
                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 位,它實際上的字節應該是:hgf + e 的最高位。

identity_hashcode:31 位,它實際上的字節應該是:e 的低 7 位 + dcb。

unused:1位,它實際上的字節應該是:a 的最高位。

age:4位,它實際上的字節應該是:a的第 4-7 位

biased_lock:1位,它實際上的字節應該是:a的第 3 位

lock:2位,它實際上的字節應該是: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

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

// 偏向鎖
                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 為了提高獲取鎖的效率和降低獲取鎖的代價,而進行的一個優化。因為 Java 團隊發現大多數的鎖都只被一個線程獲取。基于這種情況,就可以認為鎖都只被一個線程獲取,那么就不會存在多個線程競爭的條件,因此就可以不需要真正的去獲取一個完整的鎖。只需要在對象頭中寫入獲取鎖的線程 ID,用于表示該對象鎖已經被該線程獲取。

獲取偏向鎖,只要修改對象頭的標記就可以表示線程已經獲取了鎖,大大降低了獲取鎖的代價。

當線程獲取對象的偏向鎖時,它的對象頭:

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

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

epoch:用于保存偏向時間戳

age:對象 GC 年齡

biased_lock:偏向鎖標記,此時為 1

lock:鎖標記,此時為 10

獲取偏向鎖

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

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

釋放偏向鎖

線程是不會主動釋放偏向鎖的。只有當其它線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放偏向鎖。

釋放偏向鎖需要在全局安全點進行。釋放的步驟如下:

  • 暫停擁有偏向鎖的線程,判斷是否處于同步代碼塊中,如果處于,則進行偏向撤銷,并升級為輕量級鎖。

  • 如果不處于,則恢復為無鎖狀態。

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

偏向撤銷

偏向撤銷主要發生在多個線程存在競爭,不再偏向于任何一個線程了。也就是說偏向撤銷之后,將不會再使用偏向鎖。具體操作就是將 Mark Work 中的 biased_lock 由 1 設置為 0 偏向撤銷需要到達全局安全點才可以撤銷,因為它需要修改對象頭,并從棧中獲取數據。因此偏向撤銷也會存在較大的資源消耗。

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

  • 如果原持有偏向鎖的線程依然還在同步代碼塊中,那么就會將偏向鎖升級為輕量級鎖。

  • 如果原持有偏向鎖的線程已經死亡,或者已經退出了同步代碼塊,那么直接撤銷偏向鎖狀態即可。

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

批量重偏向

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

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

當一個類的大量對象被同一個線程 T1 獲取了偏向鎖,也就是大量對象先偏向于該線程 T1。T1 同步結束后,另一個線程 T2 對這些同一類型的對象進行同步操作,就會讓這些對象重新偏向于線程 T2。

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

JVM 會給對象的類對象 class 賦予兩個屬性,一個是偏向撤銷計數器,一個是 epoch 值。

我們先來看一個例子:

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 {
         //延時產生可偏向對象
        //默認4秒之后才能進入偏向模式,可以通過參數-XX:BiasedLockingStartupDelay=0設置
        Thread.sleep(5000);
        //創造100個偏向線程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線程復用,在創建完對象后,保持線程t1狀態為存活
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();
        //睡眠3s鐘保證線程t1創建對象完成
        Thread.sleep(3000);
        System.out.println("打印t1線程,list中第20個對象的對象頭:");
        System.out.println((ClassLayout.parseInstance(listA.get(19)).toPrintable()));
        //創建線程t2競爭線程t1中已經退出同步塊的鎖
        Thread t2 = new Thread(() -> {
            //這里面只循環了30次!!!
            for (int i = 0; i < 30; i++) {
                Test a = listA.get(i);
                synchronized (a) {
                    //分別打印第19次和第20次偏向鎖重偏向結果
                    if (i == 18 || i == 19) {
                        System.out.println("第" + (i + 1) + "次偏向結果");
                        System.out.println((ClassLayout.parseInstance(a).toPrintable()));
                    }
                    if (i == 10) {
                        // 該對象已經是輕量級鎖,無法降級,因此只能是輕量級鎖
                        System.out.println("第" + (i + 1) + "次偏向結果");
                        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個對象的對象頭:");
        System.out.println((ClassLayout.parseInstance(listA.get(10)).toPrintable()));
        System.out.println("打印list中第26個對象的對象頭:");
        System.out.println((ClassLayout.parseInstance(listA.get(25)).toPrintable()));
        System.out.println("打印list中第41個對象的對象頭:");
        System.out.println((ClassLayout.parseInstance(listA.get(40)).toPrintable()));
    }
}

在 JDK8 中,-XX:BiasedLockingStartupDelay 的默認值是 4000;在 JDK11 中,-XX:BiasedLockingStartupDelay 的默認值是 0

  • t1 執行完后,100 個對象都會偏向于 t1。

  • t2 執行完畢之后,其中前 19 個對象都會撤銷偏向鎖,此時類中的偏向撤銷計數器為19。但當撤銷到第 20 個的時候,偏向撤銷計數器為 20,此時達到 -XX:BiasedLockingBulkRebiasThreshold=20 的條件,于是將類中的 epoch 值 +1,并在此時找到所有處于同步代碼塊的對象,并將其 epoch 值等于類對象的 epoch 值。然后進行批量重偏向操作,從第 20 個對象開始,將會比較對象的 epoch 值是否等于類對象的 epoch 值,如果不等于,那么直接使用 CAS 替換掉 Mark Word 中的程 ID 為當前線程的 ID。

結論:

  • 前 19 個對象撤銷了偏向鎖,即 Mark Word 中的 biased_lock 為 0,如果有線程來獲取鎖,那么先獲取輕量級鎖。

  • 第 20 - 30 個對象,依然為偏向鎖,偏向于線程 t2。

  • 第 31 - 100 個對象,依然為偏向鎖,偏向于線程 t1。

tech.youzan.com/javasuo-yu-&hellip;

暫時無法在飛書文檔外展示此內容

批量撤銷偏向

當偏向鎖撤銷的數量達到 40 時,就會發生批量撤銷。但是,這是在一個時間范圍內達到 40 才會發生,這個時間范圍通過 -XX:BiasedLockingDecayTime設置,默認值為 25 秒。

也就是在發生批量偏向的 25 秒內,如果偏向鎖撤銷的數量達到了 40 ,那么就會發生批量撤銷,將該類下的所有對象都進行撤銷偏向,包括后續創建的對象。如果在發生批量偏向的 25 秒內沒有達到 40 ,就會重置偏向鎖撤銷數量,將偏向鎖撤銷數量重置為 20。

Hashcode 去哪了

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

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

Lock Record

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

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

Lock Record 只要有三個作用:

  • 持有 Displaced Word(就是對象的 Mark Word)和一些元信息用于識別哪個對象被鎖住了。

  • 解釋器使用 Lock Record 來檢測非法的鎖狀態

  • 隱式地充當鎖重入機制的計數器

那么這個 Lock Record 跟 Hashcode 有什么關系呢?

場景 1

我們先來看第一個場景:先獲取對象的 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());
    }
}

運行結果:

=====獲取 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 值之前,對象處于匿名偏向鎖狀態。鎖標記為:101

  • 步驟二:獲取對象的 hashcode 之后,對象的偏向狀態被撤銷,處于無鎖狀態。鎖標記為:001。對象頭中也存儲了 hashcode 值,hashcode 值為 0111011 10001011 10010111 00001100。

  • 步驟三:獲取鎖之后,對象處于輕量級鎖狀態。鎖標記為:00。其余 62 位為指向 Lock Record 的指針。從這里我們可以看到,Mark Word 中已經沒有 hashcode 了。整塊 Mark Word 的內容已經被復制到 Lock Word 中。

  • 步驟四:釋放鎖之后,對象處于無鎖狀態。鎖標記為:001。在 Mark Word 中也可以看到之前生成的 hashcode。與步驟二中的 Mark Word 一模一樣。這是因為在釋放鎖之后,JVM 會將 Lock Record 中的值復制回 Mark Word 中,并刪除 Lock Record。

結論:

  • 當對象生成 hashcode 之后,會撤銷偏向,并將 hashcode 記錄在 Mark Word 中。

  • 非偏向的對象獲取鎖時,會先在棧中生成一個 Lock Record。并將對象的 Mark Word 復制到 Lock Record 中。

場景2

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

運行結果:

=====獲取鎖之前=====
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 值之前,對象處于匿名偏向鎖狀態。鎖標記為:101

  • 步驟二:進入同步代碼塊,線程獲取了偏向鎖。鎖標記:101

  • 步驟三:對象生成 hashcode,此時鎖標記:10,直接從偏向鎖升級為重量級鎖。 其余 62 位為指向 objectMonitor 的指針。

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

輕量級鎖

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

獲取輕量級鎖

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

  • 禁用偏向鎖。

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

禁用偏向鎖導致升級

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

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

如果對象發生偏向撤銷時:

  • 首先會檢查持有偏向鎖的線程是否已經死亡,如果死亡,則直接升級為輕量級鎖,否則,執行步驟2

  • 查看持有偏向鎖的線程是否在同步代碼塊中,如果在,則將偏向鎖升級為輕量級鎖,否則,執行步驟3

  • 修改 Mark Word 為非偏向模式,設置為無鎖狀態。

加鎖過程

當線程獲取輕量級鎖時,首先會在線程棧中創建一個 Lock Record 的內存空間,然后拷貝 Mark Word 中的數據到 Lock Record 中。JVM 中將有數據的 Lock Record 叫做 Displated Mark Word。

Lock Record 在棧中的內存結構:

暫時無法在飛書文檔外展示此內容

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

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

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

  • 如果是,則表示當前線程已經獲取了輕量級鎖,會在棧幀中添加一個新的 Lock Record,這個新 Lock Record 中的 Displated Mark Word 為 null,owner 指向對象。這樣的目的是為了統計重入的鎖數量,因此,在棧中會有一個 Lock Record 的列表。完成這一步之后就可以直接執行同步代碼塊。

暫時無法在飛書文檔外展示此內容

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

釋放輕量級鎖

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

重量級鎖

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

獲取重量級鎖

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

  • 在持有偏向鎖的情況下,直接獲取對象的 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());
        }
    }
}

執行后的結果

=====獲取鎖之前=====
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

我們直接在偏向鎖的同步代碼塊中執行 hashcode(),會發現偏向鎖直接膨脹為重量級鎖了。我們可以看到 lock 字段為 10。

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

輕量級鎖膨脹為重量級鎖

當處于輕量級鎖的時候,說明鎖已經不再偏向于任何一個線程,但是也沒有發生競爭,可以依靠 CAS 獲取到輕量級鎖。但是當出現 CAS 獲取鎖失敗時,就會直接膨脹為重量級鎖。

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

膨脹過程

在膨脹過程中,會有幾種標記來表示鎖的狀態:

  • Inflated:膨脹已完成

  • Stack-locked:輕量級鎖

  • INFLATING:膨脹中

  • Neutral:無鎖

膨脹步驟:

檢查是否已經為重量級鎖,如果是直接返回。

檢查是否處于膨脹中的狀態,如果是,循環檢測狀態。檢測出膨脹中的狀態是因為有其他線程正在進行膨脹,因為需要等待膨脹完成之后,才能繼續執行。

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

  • 創建一個 ObjectMonitor 對象。

  • 通過 CAS 設置 Mark Word 為全 0,用以表示 INFLATING 狀態。如果失敗,則從步驟 1 重新開始執行。

  • 將 Mark Word 設置到 ObjectMonitor 對象中。

  • 設置 owner 屬性為 Lock Record

  • 設置 Mark Word 值

返回

  • 判定為無鎖狀態,執行以下步驟:

  • 創建一個 ObjectMonitor 對象。

  • 通過 CAS 直接設置 Mark Word 值。

  • 返回

競爭鎖過程

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

  • cxq:存放被阻塞的線程

  • EntryList:存放被阻塞的線程,在釋放鎖時使用

  • WaitSet:獲得鎖的線程,如果調用 wait() 方法,那么線程會被存放在此處,這是一個雙向循環鏈表

  • onwer:持有鎖的線程

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

獲取鎖過程

  • 通過 CAS 設置 onwer 為當前線程(嘗試獲取鎖),CAS 的原值為 NULL,新值為 current_thread,如果成功,則表示獲得鎖。否則執行步驟 2

  • 判斷當前線程與獲取鎖線程是否一致,如果一致,則表示獲得鎖(鎖重入)。否則執行步驟 3

  • 判斷當前線程是否為之前持有輕量級鎖的線程,如果是,直接設置 onwer 為當前線程,表示獲得鎖。否則執行步驟 4

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

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

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

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

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

為當前線程創建一個 ObjectWaiter 類型的 node 節點。步驟 i 和 ii 是一個循環,直到一個成功才會跳出這個循環。

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

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

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

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

線程被喚醒后

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

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

嘗試一輪自旋來獲取鎖。如果未獲取鎖,則跳轉回步驟 e 執行。

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

釋放重量級鎖

釋放鎖過程

判斷 _owner 字段是否等于 current_thread。如果等于則判斷當前線程是否為持有輕量級鎖的線程,如果是的話,表示該線程還沒有執行 enter()方法,因此,直接設置 _owner 字段為 current_thread。

判斷 _recursions,如果大于0,則表示鎖重入,直接返回即可,不需要執行后續解鎖代碼。

設置 _owner 字段為 NULL,解鎖成功,后續線程可以正常獲取到鎖。

喚醒其他正在被阻塞的線程。在執行以下操作之前需要使用該線程重新獲取鎖。如果獲取鎖失敗,則表示鎖已經被其他線程獲取,直接返回,不再喚醒其他線程。(為什么還要獲取到鎖才可以喚醒其他線程呢?因為喚醒線程時,需要將 cxq 中的節點轉移到 EntryList 中,涉及到鏈表的移動,如果多線程執行,將會出錯。)

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

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

線程喚醒

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

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

wait(),notify(),notifyAll()

我們需要知道一個前提,在處理 wait 方法時,必須使用重量級鎖。因此,wait 方法會導致鎖升級。

我們先來看一個例子:

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);
    }
}

最后打印的結果:

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 可以順利完成所有操作。

  • 因為 A 釋放了鎖,所以線程 B 可以獲取到鎖。然后創建了線程 C。

  • 因為線程 B 睡眠了 2S,依然持有鎖,所以線程 C 無法獲取到鎖,只能繼續等待。

  • 線程 B 調用 notify() 方法,線程 A 被喚醒,開始競爭鎖。

  • 線程 A 和線程 C 競爭鎖。

但是根據打印結果,無論執行多少次,都是線程 A 先獲取鎖。

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

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

第三個問題:執行 wait 之后,線程去哪里了?它的狀態是什么?

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

wait()

  • 膨脹為重量級鎖

  • 為 current_thread 創建 ObjectWaiter 類型的 node 節點

  • 將 node 放入 _waitSet 中

  • 釋放鎖

  • 通過 park() 阻塞 current_thread。

notify()

檢查 _waitSet 是否為 null,如果為 null,直接返回

獲取 _waitSet 的第一個元素 node,并將其從鏈表中移除。

  • 此時,存在三個策略:默認使用 policy = 2

  • 插入到 EntryList 的頭部(policy = 1)

  • 插入到 EntryList 的尾部(policy = 0)

  • 插入到 cxq 的 頭部(policy = 2)

將 node 插入到 cxq 的頭部。

notifyAll()

  • 循環檢測 _waitSet 是否不為空

  • 如果不為空,則執行 notify() 的步驟。

  • 否則返回

第一個問題:執行 wait 之后,線程去哪里了?它的狀態是什么?

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

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

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

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

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

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

到此,關于“JVM中Synchronized作用及原理是什么”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

宜川县| 巫溪县| 正蓝旗| 漯河市| 东兴市| 石屏县| 六枝特区| 察雅县| 罗平县| 梁平县| 鹤峰县| 扶绥县| 井研县| 甘洛县| 启东市| 林西县| 贵定县| 金湖县| 德保县| 昌图县| 商城县| 刚察县| 锡林郭勒盟| 桐梓县| 班戈县| 客服| 阿拉善盟| 安乡县| 武隆县| 阿坝县| 安多县| 定边县| 长宁县| 洱源县| 德阳市| 灵宝市| 万盛区| 溧阳市| 河东区| 澄江县| 清苑县|