首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >多线程基础(五):java对象的MarkWord及synchronized锁升级过程

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

作者头像
冬天里的懒猫
发布于 2020-09-10 02:19:26
发布于 2020-09-10 02:19:26
1K0
举报

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

1.java对象的内存布局

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

1.1 导入jol

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

代码语言:javascript
AI代码解释
复制
<!-- 查看内存布局-->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

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

1.2 空对象的内存布局

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

代码语言:javascript
AI代码解释
复制
public class SynchronizedTest {

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

}

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

代码语言:javascript
AI代码解释
复制
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个字节实际上是空的,在此只是为了对齐所用。

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

代码语言:javascript
AI代码解释
复制
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

1.3 数组的对象布局

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

代码语言:javascript
AI代码解释
复制
public class SynchronizedTest {

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

}

其输出:

代码语言:javascript
AI代码解释
复制
[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加锁,再看看结果:

代码语言:javascript
AI代码解释
复制
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);
		}
	}

}

输出结果如下:

代码语言:javascript
AI代码解释
复制
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如下表:

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

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

代码语言:javascript
AI代码解释
复制
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锁升级的过程:

需要注意的是:

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

3.synchronized的字节码

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

代码语言:javascript
AI代码解释
复制
$ 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的加锁和升级过程进行分析。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/09/07 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
多线程四 并发中锁的原理
偏向锁就是在运行过程中,对象的锁偏向某个线程,即在开启偏向锁的情况下,某个线程获得锁,当该线程下次想要获得锁时,不需要再获取锁(忽略synchronized关键字),直接执行代码
用针戳左手中指指头
2021/01/29
6900
多线程四 并发中锁的原理
[JAVA基础] - JVM对象内存布局及锁的标记位
一、对象布局 1、对象头 1)存储对象自身的运行时数据 hash码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。占位32/64位虚拟机分别占32/64个比特,官方称"Mark Word" 2)类型指针 指向对象的元数据,如果是数组,还会存储数组长度。 2、实例数据 3、对齐填充 要求对象是8的整数倍,对象头已经是8位的整数倍,只填充实例数据即可。 二、Object o = new Object()内存占用情况 占用16个字节 对象头12个字节,对齐填充4个字
夹胡碰
2022/05/19
5510
[JAVA基础] - JVM对象内存布局及锁的标记位
并发基石-Markword与锁升级
synchronized关键字是java提供的互斥锁关键字,我们常说的互斥锁一般都是非自旋锁,即竞争不到锁的线程会进入阻塞状态知道被唤醒 今天我们来讲讲java中用来对synchronized进行优化的三种锁,同时会介绍markword对象头 目前我在网上搜到的十几篇博客讲的都有问题,可能有写对的我没搜到. 很多人不经过验证直接把markOop.hpp中的JavaThread*当成ThreadId这是错误的,实际是java线程在C语言的指针 并且未计算过hashCode和计算过hashCode的情况也是不一样的 本篇博客最后会展示使用jol工具,读取展示对象头的结果进行验证 附上openjdk的markOop.hpp链接
歪歪梯
2020/06/19
6632
JAVA对象布局之对象头(Object Header)
2.调用ClassLayout.parseInstance().toPrintable()
Java宝典
2020/12/04
1.7K1
多线程五 锁的膨胀过程
上一篇中,涉及到了锁升级的过程,也对其锁的升级有了一个大概的了解:单线程持有,在jvm延迟偏向的时间内是轻量级锁,之后为偏向锁,出现多个线程交替执行,对同一资源加锁会升级为轻量级锁,多个线程竞争拿不到锁会升级为重量级锁。在上一篇的基础上再进一步的了解锁升级的过程。
用针戳左手中指指头
2021/01/29
3540
CAS、ABA问题、锁升级
1.首先读取该值: 假如该值N初始状态为0,那么读取该值以后保存到自定义的变量E中
孑小白
2021/05/13
5280
CAS、ABA问题、锁升级
初步了解Java对象布局
其中byte、short、int、long都是表示整数的,只不过他们的取值范围不一样
iiopsd
2023/10/17
2970
初步了解Java对象布局
synchronized底层实现知多少?synchronized加锁还会升级?
线程2将count减到了97,线程3、线程1在某一刻也做了count--,但是结果却也是97,说明他们在做count--的时候并不知道有别的线程也操作了count。
行百里er
2020/12/02
5240
synchronized底层实现知多少?synchronized加锁还会升级?
面试专题:Synchronized 锁的升级过程(锁/对象状态)及底层原理
这个面试题其实涉及到的底层知识比较多,在Java中都知道synchronized,这是一个关键字,为什么使用了之后,可以结果多线程安全问题。里面内部流程是怎样的呢?加锁是加在哪里?金三银四越来越卷,面试官不再是,单纯的问如何解决线程安全,有没有使用过synchronized,而是想知道synchronized底层的知识点。本文就深入讲解synchronized底层原理,对象加锁是如果一步一步实现的。
小明爱吃火锅
2024/02/18
1.6K0
面试专题:Synchronized 锁的升级过程(锁/对象状态)及底层原理
对象实例化与内存布局(深入)
创建对象方式有:new、Class的newInstance()、Constructor的newInst(Xxx)、使用clone()、使用反序列化、第三方库Objenesis;
逍遥壮士
2021/05/24
1.3K0
对象实例化与内存布局(深入)
打印Java对象头
这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。 为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:
@阿诚
2021/02/04
2.8K0
synchronized 底层如何实现?什么是锁升级、降级?
synchronized 代码块是由一对 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现单元。
王小明_HIT
2020/05/08
1.6K0
synchronized 底层如何实现?什么是锁升级、降级?
Synchronized 原理与锁升级
锁对象为 this,校验锁对象就是 this 改造一下上方的同步方法,用 synchronized 加上锁对象的方式来校验锁对象就是 this
程序员NEO
2023/09/30
3570
Synchronized 原理与锁升级
JVM - 剖析Java对象头Object Header之对象大小
JVM - 写了这么多年代码,你知不道new对象背后的逻辑? 中大体介绍了Java中 new 对象背后的主要流程,其中对象头的部分,我们仅仅是点到为止,这里我们深入剖一下Object Header的奥秘 。
小小工匠
2021/08/17
1.8K0
(Java并发编程——JUC)共享问题解决与synchronized对象锁分析!全程干货!!快快收藏!!
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
凉凉心.
2025/10/13
1350
(Java并发编程——JUC)共享问题解决与synchronized对象锁分析!全程干货!!快快收藏!!
4. synchronized详解
  多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是:对象、变量、文件等。
用户7798898
2020/09/27
5790
4. synchronized详解
线程并发带来的安全性问题 之 同步锁(一)
在下面的案例中,演示了两个线程分别去去调用 demo.incr 方法来对 i 这个变量进行叠加,预期结果 应该是20000,但是实际结果却是小于等于20000的值。
架构探险之道
2023/03/04
3310
线程并发带来的安全性问题 之 同步锁(一)
深入了解Java锁
继 打印Java对象头,我们再深入探索一下Java锁。无锁状态我们就不说了,下面我们一一打印偏向锁、轻量锁,重量锁的对象头。
@阿诚
2021/02/04
5190
Java 对象在内存
1. Mark word: 记录线程,锁等对象状态,64位机占用8字节;32位机占用4字节; 当前主机是64位占8字节
一个架构师
2022/06/20
3200
Java 对象在内存
synchronized 锁的升级过程
测试代码 static A obj; // -XX:BiasedLockingStartupDelay=0 偏向锁开关 // -XX:+PrintFlagsInitial 打印所有参数 public static void main(String[] args) throws InterruptedException { obj = new A(); // Thread.sleep(60000); System.out.println(ClassLayout.parseInstance(ob
付威
2021/03/07
7230
相关推荐
多线程四 并发中锁的原理
更多 >
交个朋友
加入腾讯云官网粉丝站
双11活动抢先看 更有社群专属礼券掉落
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档