多線程基礎(chǔ)(五):java對象的MarkWord及synchronized鎖升級過程

[toc]

在前面聊過了如何使用synchronized,以及synchronized不同的加鎖方式分別鎖的是哪些對象。本文對synchronized底層的原理進(jìn)行深層次的分析。

1.java對象的內(nèi)存布局

再前面學(xué)習(xí)了JMM之后,做為一個java程序員,肯定最大的疑問在于,一個java對象,究竟再內(nèi)存中是如何存儲的?因此,我們需要用到一個三方的jar包工具jol來對java對象進(jìn)行查看。

1.1 導(dǎo)入jol

導(dǎo)入的方式比較簡單,我們只需要在pom文件中添加如下內(nèi)容即可:

<!-- 查看內(nèi)存布局-->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

之后就可以使用jol來查看對象的內(nèi)存布局了。

1.2 空對象的內(nèi)存布局

首先我們來查看一個Object空對象的內(nèi)存布局:

public class SynchronizedTest {

    public static void main(String[] args) {
        Object o = new Object();
        String s = ClassLayout.parseInstance(o).toPrintable();
        System.out.println(s);
    }

}

執(zhí)行上述代碼,將輸出如下內(nèi)容:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到,輸出結(jié)果一共有4行,輸出結(jié)果分別是OFFSET表示開始的偏移量,SIZE表示大小。我們可以看到,前三行都是object header。表示對象的頭文件。而前面的兩行是對象頭markword。第三行的4個字節(jié)是對象指針。由于該對象是一個空對象,那么最后的4個字節(jié)實際上是空的,在此只是為了對齊所用。


image.png

需要注意的是,在java中,對象指針默認(rèn)是可以壓縮的。我們可以用-XX:-UseCompressedClassPointers來關(guān)閉,那么此時對象指針就有8個字節(jié)。


image.png
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           00 1c fd 1d (00000000 00011100 11111101 00011101) (503127040)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
image.png

1.3 數(shù)組的對象布局

在java中,數(shù)組實際上是一個特殊的對象,我們來看看數(shù)組的對象布局:

public class SynchronizedTest {

    public static void main(String[] args) {
        Object [] o = new Object[10];
        String s = ClassLayout.parseInstance(o).toPrintable();
        System.out.println(s);
    }

}

其輸出:

[Ljava.lang.Object; object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           4c 23 00 f8 (01001100 00100011 00000000 11111000) (-134208692)
     12     4                    (object header)                           0a 00 00 00 (00001010 00000000 00000000 00000000) (10)
     16    40   java.lang.Object Object;.<elements>                        N/A
Instance size: 56 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

可以發(fā)現(xiàn),數(shù)組對象其header中會多一行,第四行,其中存的是數(shù)組的長度。在此時輸出為10。

1.4 synchronized之后的對象布局

我們現(xiàn)在來測試將object加鎖,再看看結(jié)果:

public class SynchronizedTest {

    public static void main(String[] args) {
        Object o = new Object();
        synchronized (o) {
            String s = ClassLayout.parseInstance(o).toPrintable();
            System.out.println(s);
        }
    }

}

輸出結(jié)果如下:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           d0 f5 e4 04 (11010000 11110101 11100100 00000100) (82114000)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到,MarkWord明顯不同于前面的情況。第一行中的值發(fā)生了明顯的變化。因此,synchronized實際上是通過修改MarkWord的值來實現(xiàn)其加索的。
實際上這一點也非常好理解,如果需要對Object對象加鎖,那么最簡單的辦法就是在這個對象的MarkWord上做一個標(biāo)記。至于加鎖的細(xì)節(jié),我們來詳細(xì)對MarkWord進(jìn)行分析。

2.MarkWord

通過前面部分的內(nèi)容,不難發(fā)現(xiàn),再java對象中,有個關(guān)鍵的內(nèi)容就是對象頭中的MarkWord部分。
實際上,對于markWord的控制,一共有5種情況。
需要注意的是,MarkWord小端在前。
MarkWord分別對應(yīng)五種狀態(tài)。64bit的MarkWord如下表:


64bit MarkWord

但是有的版本32位的jdk也是采用的32bit的MarkWord。


32bit MarkWord

上述五種狀態(tài)分別是:無鎖、偏向鎖、輕量級鎖、重量級鎖、GC回收之后的標(biāo)記。
上圖中的epoch,是偏向鎖的時間戳。
我們再來對比之前執(zhí)行的結(jié)果。
空對象的結(jié)果:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到第一個字節(jié)的最后一位是1。為什么不是第二個字節(jié)的最后一位呢,按上表的描述,最后兩個字節(jié)為01表示無鎖。但是需要注意的是,jvm采用的是小端模式,數(shù)據(jù)的高字節(jié)存儲再高地址中,低字節(jié)存儲再低地址中。但是需要注意的是,這里每次輸出的都是4個字節(jié),再第一行的內(nèi)部,jol已經(jīng)幫我們做了處理。因此現(xiàn)在看起來第一行的最后兩位才是我們上表中的鎖狀態(tài)位。

3.synchronized的鎖升級簡介

再synchronized的執(zhí)行過程中,實際上一個對象的狀態(tài)就如上表所示進(jìn)行變化:

  • 無鎖:所有對象創(chuàng)建的時候都是無鎖狀態(tài)。此時MarkWord上只有一個標(biāo)識,沒有其他內(nèi)容。
  • 偏向鎖:如果我們需要對一個無鎖的對象加鎖,那么最初始的操作非常簡單,通過cas操作在其MarkWord上修改偏向鎖狀態(tài)為1,之后將線程的ID和epoch存儲在MarkWord中。偏向鎖是采用cas操作的,只有遇到其他線程競爭的時候,才會釋放。
  • 輕量級鎖:當(dāng)鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。當(dāng)加了偏向鎖的對象,有其他線程也參與其鎖的競爭的時候,此時,就會將偏向鎖撤銷,然后再判斷是否需要變成輕量級鎖。此時也是通過cas操作,將鎖標(biāo)識位修改為00。并將指向棧中記錄的指針寫入markWord中。
  • 重量級鎖:當(dāng)多個線程競爭同一個鎖的時候,虛擬機會阻塞加鎖失敗的線程,并將在目標(biāo)被鎖釋放的時候,喚醒這個線程。java線程的阻塞與喚醒,都是依賴于系統(tǒng)操作os pthread_mutex_lock() 。當(dāng)升級為重量級的鎖之后,鎖的標(biāo)識狀態(tài)為10,此時MarkWord中存儲的是指向重量級鎖的指針。其他的等待線程都會進(jìn)入阻塞狀態(tài)。
  • GC狀態(tài):標(biāo)記之后等待GC回收的對象。

這就是synchronized鎖升級的過程:


image.png

需要注意的是:

  • 偏向鎖只會在第一次請求的時候采用cas操作,修改鎖的對象和記錄線程的地址。在之后的運行過程中,持有該偏向所的線程再次加鎖就會直接返回。偏向鎖僅僅只針對同一線程持有鎖的情況。
  • 輕量級鎖采用cas操作,將鎖的對象標(biāo)記字段替換為一個指針,指向當(dāng)前線程棧上的一塊空間。存儲著鎖對象原本的標(biāo)記字段。他針對的是多個線程在不同時間段同時請求同一個鎖的情況。
  • 重量級鎖實際上通過系統(tǒng)調(diào)用0x80操作,會阻塞其他線程,針對的是多個線程同時競爭同一個鎖的情況,java虛擬機采用了自適應(yīng)的自旋操作,避免線程進(jìn)行不必要的阻塞和喚醒的情況。

3.synchronized的字節(jié)碼

我們通過javap來看看前文中的SynchronizedTest.class的內(nèi)容

$ javap -c -l SynchronizedTest
????: ?????????SynchronizedTest????com.dhb.test.SynchronizedTest
Compiled from "SynchronizedTest.java"
public class com.dhb.test.SynchronizedTest {
  public com.dhb.test.SynchronizedTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
    LineNumberTable:
      line 6: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0  this   Lcom/dhb/test/SynchronizedTest;

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/Object
       3: dup
       4: invokespecial #1                  // Method java/lang/Object."<init>":()V
       7: astore_1
       8: aload_1
       9: dup
      10: astore_2
      11: monitorenter
      12: aload_1
      13: invokestatic  #3                  // Method org/openjdk/jol/info/ClassLayout.parseInstance:(Ljava/lang/Object;)Lorg/openjdk/jol/info/ClassLayout;
      16: invokevirtual #4                  // Method org/openjdk/jol/info/ClassLayout.toPrintable:()Ljava/lang/String;
      19: astore_3
      20: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      23: aload_3
      24: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      27: aload_2
      28: monitorexit
      29: goto          39
      32: astore        4
      34: aload_2
      35: monitorexit
      36: aload         4
      38: athrow
      39: return
    Exception table:
       from    to  target type
          12    29    32   any
          32    36    32   any
    LineNumberTable:
      line 9: 0
      line 10: 8
      line 11: 12
      line 12: 20
      line 13: 27
      line 14: 39
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
         20       7     3     s   Ljava/lang/String;
          0      40     0  args   [Ljava/lang/String;
          8      32     1     o   Ljava/lang/Object;
}

可以發(fā)現(xiàn),在輸出結(jié)果中,synchronized的本質(zhì),實際上是轉(zhuǎn)換為了monitorenter和兩個monitorexit字節(jié)碼。之所以有兩個字節(jié)碼是因為需要對正常和異常兩條路徑都確保能夠monitorexit退出。
monitorenter和monitorexit指令都是在hotSpot源碼的objectMonitor.cpp中。后續(xù)將通過源碼,對synchronized的加鎖和升級過程進(jìn)行分析。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容