多线程基础(五):java对象的MarkWord及synchronized锁升级过程

[toc]

在前面聊过了如何使用synchronized,以及synchronized不同的加锁方式分别锁的是哪些对象。本文对synchronized底层的原理进行深层次的分析。

1.java对象的内存布局

再前面学习了JMM之后,做为一个java程序员,肯定最大的疑问在于,一个java对象,究竟再内存中是如何存储的?因此,我们需要用到一个三方的jar包工具jol来对java对象进行查看。

1.1 导入jol

导入的方式比较简单,我们只需要在pom文件中添加如下内容即可:

<!-- 查看内存布局-->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

之后就可以使用jol来查看对象的内存布局了。

1.2 空对象的内存布局

首先我们来查看一个Object空对象的内存布局:

public class SynchronizedTest {

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

}

执行上述代码,将输出如下内容:

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

可以看到,输出结果一共有4行,输出结果分别是OFFSET表示开始的偏移量,SIZE表示大小。我们可以看到,前三行都是object header。表示对象的头文件。而前面的两行是对象头markword。第三行的4个字节是对象指针。由于该对象是一个空对象,那么最后的4个字节实际上是空的,在此只是为了对齐所用。


image.png

需要注意的是,在java中,对象指针默认是可以压缩的。我们可以用-XX:-UseCompressedClassPointers来关闭,那么此时对象指针就有8个字节。


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 数组的对象布局

在java中,数组实际上是一个特殊的对象,我们来看看数组的对象布局:

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

可以发现,数组对象其header中会多一行,第四行,其中存的是数组的长度。在此时输出为10。

1.4 synchronized之后的对象布局

我们现在来测试将object加锁,再看看结果:

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

}

输出结果如下:

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明显不同于前面的情况。第一行中的值发生了明显的变化。因此,synchronized实际上是通过修改MarkWord的值来实现其加索的。
实际上这一点也非常好理解,如果需要对Object对象加锁,那么最简单的办法就是在这个对象的MarkWord上做一个标记。至于加锁的细节,我们来详细对MarkWord进行分析。

2.MarkWord

通过前面部分的内容,不难发现,再java对象中,有个关键的内容就是对象头中的MarkWord部分。
实际上,对于markWord的控制,一共有5种情况。
需要注意的是,MarkWord小端在前。
MarkWord分别对应五种状态。64bit的MarkWord如下表:


64bit MarkWord

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


32bit MarkWord

上述五种状态分别是:无锁、偏向锁、轻量级锁、重量级锁、GC回收之后的标记。
上图中的epoch,是偏向锁的时间戳。
我们再来对比之前执行的结果。
空对象的结果:

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

可以看到第一个字节的最后一位是1。为什么不是第二个字节的最后一位呢,按上表的描述,最后两个字节为01表示无锁。但是需要注意的是,jvm采用的是小端模式,数据的高字节存储再高地址中,低字节存储再低地址中。但是需要注意的是,这里每次输出的都是4个字节,再第一行的内部,jol已经帮我们做了处理。因此现在看起来第一行的最后两位才是我们上表中的锁状态位。

3.synchronized的锁升级简介

再synchronized的执行过程中,实际上一个对象的状态就如上表所示进行变化:

  • 无锁:所有对象创建的时候都是无锁状态。此时MarkWord上只有一个标识,没有其他内容。
  • 偏向锁:如果我们需要对一个无锁的对象加锁,那么最初始的操作非常简单,通过cas操作在其MarkWord上修改偏向锁状态为1,之后将线程的ID和epoch存储在MarkWord中。偏向锁是采用cas操作的,只有遇到其他线程竞争的时候,才会释放。
  • 轻量级锁:当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。当加了偏向锁的对象,有其他线程也参与其锁的竞争的时候,此时,就会将偏向锁撤销,然后再判断是否需要变成轻量级锁。此时也是通过cas操作,将锁标识位修改为00。并将指向栈中记录的指针写入markWord中。
  • 重量级锁:当多个线程竞争同一个锁的时候,虚拟机会阻塞加锁失败的线程,并将在目标被锁释放的时候,唤醒这个线程。java线程的阻塞与唤醒,都是依赖于系统操作os pthread_mutex_lock() 。当升级为重量级的锁之后,锁的标识状态为10,此时MarkWord中存储的是指向重量级锁的指针。其他的等待线程都会进入阻塞状态。
  • GC状态:标记之后等待GC回收的对象。

这就是synchronized锁升级的过程:


image.png

需要注意的是:

  • 偏向锁只会在第一次请求的时候采用cas操作,修改锁的对象和记录线程的地址。在之后的运行过程中,持有该偏向所的线程再次加锁就会直接返回。偏向锁仅仅只针对同一线程持有锁的情况。
  • 轻量级锁采用cas操作,将锁的对象标记字段替换为一个指针,指向当前线程栈上的一块空间。存储着锁对象原本的标记字段。他针对的是多个线程在不同时间段同时请求同一个锁的情况。
  • 重量级锁实际上通过系统调用0x80操作,会阻塞其他线程,针对的是多个线程同时竞争同一个锁的情况,java虚拟机采用了自适应的自旋操作,避免线程进行不必要的阻塞和唤醒的情况。

3.synchronized的字节码

我们通过javap来看看前文中的SynchronizedTest.class的内容

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

可以发现,在输出结果中,synchronized的本质,实际上是转换为了monitorenter和两个monitorexit字节码。之所以有两个字节码是因为需要对正常和异常两条路径都确保能够monitorexit退出。
monitorenter和monitorexit指令都是在hotSpot源码的objectMonitor.cpp中。后续将通过源码,对synchronized的加锁和升级过程进行分析。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容