Java synchronized锁升级过程验证

Java对象结构

Java synchronized锁升级过程验证

一个对象包括三部分:

对象头

实例数据

对其填充

对象头:

Mark Word:用于存储对象自身运行时的数据,如哈希码(Hash Code),GC分代年龄,锁状态标志,偏向线程ID、偏向时间戳等信息,它会根据对象的状态复用自己的存储空间。它是实现轻量级锁和偏向锁的关键。

Klass Pointer:存储指向方法区对象类型指针

Array Length:如果是数组,还包括数组长度

如果对象为非数组类型,用2字宽存储对象头。

如果对象为数组类型,用3字宽存储对象头。

在32位虚拟机中,1字宽等于4字节,即32bit。在64位虚拟机中,1字宽等于8字节,即64bit。如下表所示:

Java synchronized锁升级过程验证

实例数据

存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐;

对齐 填充

不是必须部分,由于虚拟机要求对象起始地址必须是8字节的整数倍,对齐填充仅仅是为了使字节对齐。

对象头结构

Mark Word:

Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。

32位JVM 的Mark Word的默认存储结构如下:

Java synchronized锁升级过程验证

在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变 化为存储以下4种数据

Java synchronized锁升级过程验证

完整结构:

Java synchronized锁升级过程验证

在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:

Java synchronized锁升级过程验证

这里我们主要关注这3个部分:锁状态、是否偏向锁、锁标志位。

锁标记位(lock):该标记值表示对象锁的状态。

是否为偏向锁(biased_lock):对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。

class pointer:

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。而64位的对象头有点浪费空间,JVM默认会开启指针压缩,所以基本上也是按32位的形式记录对象头的。

开启压缩指针(-XX:+UseCompressedOops) 关闭压缩指针(-XX:-UseCompressedOops)

,其中,oop即ordinary object pointer普通对象指针。

array length:

如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。

synchronized锁

介绍完对象头,现在我们来介绍synchronized关键字。synchronized是对象锁,锁状态变化就体现在上面介绍的Mark Word中的偏向锁以及锁标志位。

锁升级介绍:

我们先介绍下锁升级过程。

JD6之后分为无锁,偏向锁,轻量级锁,重量级锁。其中偏向锁->轻量级锁->重量级锁的升级过程不可逆。

偏向锁:当一个线程第一次获取到锁之后,再次申请就可以直接取到锁

核心思想:

一开始无锁状态,JVM会默认开启”匿名”偏向的一个状态,就是一开始线程还未持有锁的时候,就预先设置一个匿名偏向锁,等一个线程持有锁之后,就会利用CAS操作将线程ID设置到对象的mark word 的高54位上【64位虚拟机】。如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。

轻量级锁:没有多线程竞争,但有多个线程交替执行。

轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁将会升级为轻量级锁,Mark Word 的结构也变为轻量级锁的结构。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。

执行同步代码块之前,JVM会在线程的栈帧中创建一个锁记录(Lock Record),并将Mark Word拷贝复制到锁记录中。然后尝试通过CAS操作将Mark Word中的锁记录的指针,指向创建的Lock Record。如果成功表示获取锁状态成功。如果失败,则进入自旋获取锁状态。如果自旋获取锁也失败了,则升级为重量级锁,也就是把线程阻塞起来,等待唤醒。

自旋锁与自适应自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。

自旋锁:许多情况下,当线程没有获得monitor对象的所有权时,就会进入阻塞,当持有锁的线程释放了锁,当前线程才可以再去竞争锁,但是如果按照这样的规则,就会浪费大量的性能在阻塞和唤醒的切换上,特别是线程占用锁的时间很短的话。

为了避免阻塞和唤醒的切换,在没有获得锁的时候就不进入阻塞,而是不断地循环检测锁是否被释放,这就是自旋。在占用锁的时间短的情况下,自旋锁表现的性能是很高的。

但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,那么自旋的次数就会变多,占用cpu时间变长导致性能变差。当然我们也可以设置自旋锁的自旋次数,当自旋一定的次数(时间)后就挂起。但是如果设置次数少了或者多了都会导致性能受到影响,所以在JDK1.6引入了自适应性自旋锁。

自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

表现是如果此次自旋成功了,很有可能下一次也能成功,于是允许自旋的次数就会更多,反之,如果很少有线程能够自旋成功,很有可能下一次也是失败,则自旋次数就更少。这样能最大化利用资源,随着程序运行和性能监控信息的不断完善,虚拟机对锁的状况预测会越来越准确,也就变得越来越智能。

重量级锁:有多线程竞争,线程获取不到锁进入阻塞状态。

重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。

锁升级验证

锁升级过程是非常复杂的,很多理论知识很难用实践验证,这里我们只验证锁状态的变化过程,也就是Mark Word中锁标志位的变化。

这里我们引用一个 Maven 依赖 jol(Java Object Layout),这个类提供了工具方法可以打印虚拟机状态。

   <dependency>
        <groupId>org.openjdk.jolgroupId>
        <artifactId>jol-coreartifactId>
        <version>0.10version>
    dependency>

接下来查看虚拟机信息。

System.out.println(VM.current().details());

Java synchronized锁升级过程验证

开启偏向锁

我们设计两个线程,线程0和线程1,分别获取对象锁,然后打印对象头看锁状态变化。

先看开启偏向锁的情况:

-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

Java synchronized锁升级过程验证

无锁竞争,偏向锁->轻量级锁

Java synchronized锁升级过程验证

执行结果:

Java synchronized锁升级过程验证

这里先介绍下如何看对象头状态。前面说过64位jvm对象头的Mark Word占8个字节,所以这里05 00 00 00 00 00 00 00都是Mark Word。由于jvm采用大端模式存储字节,将高位字节存放在低地址,将低位字节存放在高地址,所以这里对照对象头表格来看要倒序,即00000101对应这8位。可以看到这时对象处于偏向锁状态。

Java synchronized锁升级过程验证

Java synchronized锁升级过程验证

Java synchronized锁升级过程验证

Java synchronized锁升级过程验证

分析执行结果:

线程0和线程1无并发冲突,线程0两次都是获取的偏向锁,验证了前面关于偏向锁的定义。线程1获取锁的时候发现当前占有锁的是线程0,于是升级为轻量级锁。

有锁竞争,偏向锁->重量级锁

Java synchronized锁升级过程验证

执行结果:

Java synchronized锁升级过程验证

Java synchronized锁升级过程验证

Java synchronized锁升级过程验证

Java synchronized锁升级过程验证

分析执行结果:

线程0和线程1存在锁竞争,于是从偏向锁升级为重量级锁。

关闭偏向锁

-XX:-UseBiasedLocking:

Java synchronized锁升级过程验证

无锁竞争,无锁->轻量级锁:

Java synchronized锁升级过程验证

Java synchronized锁升级过程验证

Java synchronized锁升级过程验证

Java synchronized锁升级过程验证

执行结果分析:

关闭偏向锁,默认是无锁状态。无锁竞争,从无锁状态升级为轻量级锁。

有锁竞争,无锁->重量级锁:

Java synchronized锁升级过程验证

Java synchronized锁升级过程验证

Java synchronized锁升级过程验证

Java synchronized锁升级过程验证

执行结果分析:

关闭偏向锁,默认是无锁状态。有锁竞争,从无锁状态升级为重量级锁。

Original: https://www.cnblogs.com/heimuye/p/16632534.html
Author: 黑木爷
Title: Java synchronized锁升级过程验证

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/609418/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 长篇图解etcd核心应用场景及编码实战

    大家好啊,我是字母哥,今天写一篇关于etcd的文章,其实网上也有很多关于etcd的介绍, 我就简明扼要,总结提炼,期望大家通过这一篇文章掌握etcd的核心知识以及编码技能! 本文首…

    Java 2023年6月15日
    077
  • Elastic:菜鸟上手指南

    Elastic:菜鸟上手指南 我们可以按照如下的步骤来学习: 11) Core Stack: 12) 中文分词器介绍: 13) Aggregations 14) Painless …

    Java 2023年5月29日
    075
  • 三分钟学会短信验证

    一:打开APISpace官网,登录,搜索短信验证,点击立即购买,新用户会送十条短信 https://www.apispace.com/ 二:打开我的Api,找到刚刚购买的短信流量包…

    Java 2023年6月15日
    074
  • 20年5月面试汇总

    redis: 数据结构 redis的五种基本数据结构: string、hash、set、zset、list、HyperLogLog…. 补充:BloomFilter等 …

    Java 2023年6月8日
    069
  • Java动态脚本Groovy获取Bean(奇淫技巧操作)

    前言:请各大网友尊重本人原创知识分享,谨记本人博客: 南国以南i 背景: 在Java代码中当我们需要一个Bean对象,通常会使用spring中@Autowired注解,用来自动装配…

    Java 2023年6月5日
    076
  • Prim算法(三)之 Java详解

    普里姆(Prim)算法,是用来求加权连通图的最小生成树的算法。 基本思想对于图G而言,V是所有顶点的集合;现在,设置两个新的集合U和T,其中U用于存放G的最小生成树中的顶点,T存放…

    Java 2023年5月29日
    056
  • 记录一下今天所学 9.22

    今天上午在公司没啥任务,就学起了es,看的黑马的资料,先看文档,不懂的地方就去看了下视频。 大概知道了es概念,es是es技术栈中最核心的,这个技术栈还有其他的比如分词器插件,还有…

    Java 2023年6月15日
    065
  • SSM项目的登录功能controller层的实现

    1.需求分析 用户在登录页面,输入用户名和密码,点击”登录”按钮或者回车,完成用户登录的功能. 用户名和密码不能为空 用户名或者密码错误 , 用户已过期 ,…

    Java 2023年6月9日
    070
  • AIX系统下挂载外置存储

    连接盘柜后在盘柜里映射好分区。 1.扫描硬件才能发现盘柜映射的容量 ,命令cfgmgr 2、查看在 AIX 系统下能否认到盘柜的分区。 命令:lsdev -Cc disk 3、查看…

    Java 2023年6月8日
    078
  • 【Java面试】3年经验,这个问题该怎么回答 Mybatis是如何进行分页的?

    “Mybatis是如何进行分页的”?这是一个工作了3年的同学,在面试的时候遇到的问题。大家好,我是Mic,一个工作了14年的Java程序员。经常有同学在后台…

    Java 2023年6月16日
    075
  • 页面国际化

    页面国际化 有的时候,我们的网站会去涉及中英文甚至多语言的切换,这时候我们就需要对页面进行国际化设计了。 6.1 准备工作 在IDEA中统一设置properties的编码格式 6….

    Java 2023年6月5日
    052
  • 用 Vim 编辑 Markdown 时直接粘贴图片

    我习惯使用 Vim 编辑 Markdown 文件,一直存在一个痛点就是粘贴图片很不方便。 前后对比 我以前常用的操作流程: 复制图片/截图; 在保存图片对话框里一层层点选保存路径,…

    Java 2023年6月5日
    070
  • 创建Springboot工程接收acticemq消息

    1、JMSFactory配置 <beans xmlns="http://www.springframework.org/schema/beans" xml…

    Java 2023年5月30日
    094
  • 基本运算符

    运算符 JAVA语言支持入下运算符。 %:余数 !=:不等于 &&:and ||:or !:not 二元运算符 整数默认为int类型,按类型优先级自动转换,下图说明…

    Java 2023年6月9日
    076
  • junit结合spring-test里的MockMvc来测试SpringMvc接口方法

    如下是SpringMvc项目里的Controller 重点来了,junit结合spring-test里的MockMvc来测试上面的http接口 运行结果(支持debug调试,是不是…

    Java 2023年5月29日
    0150
  • 递归

    总结: 递归是一个思想,自己调用自己的一个过程 要点: 分析: 3.书写递归就是 ​ 找出什么时候不再 调用自己本身的条件 和 书写需要循环执行的代码 代码需要逻辑严密 4.注意事…

    Java 2023年6月8日
    065
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球