当前位置: 首页 > news >正文

多线程-锁升级和对象的内存布局

简介

锁升级,也可以理解为synchronized关键字的原理,是Java1.6中对synchronized关键字做的优化,锁升级涉及到对象的内存布局,在这里总结一下锁升级的过程和过程中对象内存布局的变化。

锁升级

锁升级:在JDK 1.6之前,Java内置锁还是一个重量级锁,是一个效率比较低下的锁,在JDK 1.6之后,为了提高锁的获取与释放效率,对synchronized的实现进行了优化,引入了偏向锁和轻量级锁,从此Java内置锁就有4种状态,级别由低到高依次为:无锁、偏向锁、轻量级锁和重量级锁。锁的4种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。

锁升级的过程:

  • 无锁:对象刚创建的时候,处于无锁状态

  • 偏向锁:只有一个线程获取锁时,发现锁是无锁状态并且是可偏向的,会尝试把自己的线程id写入到对象头中,表示当前的锁是一个偏向锁。偏向锁的执行效率和无锁几乎相同,因为它只有第一次获取锁时,使用CAS算法,将线程ID设置到对象头中,之后,如果发现这个线程ID是自己,直接获取锁。由于偏向锁是为单个线程设计的,所以线程不会主动释放偏向锁,只有遇到其他线程尝试竞争偏向锁时,偏向锁才会被撤销。

  • 轻量级锁:偏向锁在有锁竞争时升级为轻量级锁。在轻量级锁下,没有获取到锁的线程不会阻塞而是自旋,如果同步代码块的执行时间较短并且竞争不激烈,自旋比阻塞导致的线程上下文切换带来的消耗要小。所以轻量级锁是在竞争不激烈的情况下使用自旋代替互斥。

  • 重量级锁:线程的自旋次数超过阈值之后,锁由轻量级锁升级为重量级锁,此时等待锁的线程都会进入阻塞状态,并且存放于一个监视器中,重量级锁的底层依赖monitor机制。

偏向锁的延迟加载:虚拟机在启动5秒之内创建的锁是轻量级锁,5秒之后创建的锁为偏向锁,这是虚拟机的优化措施。JVM会在一个安全点暂停持有偏向锁的线程,然后将用作锁的对象头中的偏向锁标识撤销,在这个安全点所有线程都停止工作,所以偏向锁的撤销可能会导致STW(世界暂停),影响服务的稳定性。

锁的升级是由JVM来进行的。

对象的内存布局

对象的内存布局描述了对象的数据结构。

查看java对象内存布局的方式:在项目中添加jar包 jol-core,jol的含义是java object layout,它是openjdk提供的工具包,可以用来查看对象的内存布局。

入门案例

查看一个Object类型的对象在内存中的布局,这算是最简单的对象了

步骤:第一步添加依赖、第二步打印对象的内存布局

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>
public static void main(String[] args) {
    Object obj = new Object();
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}

结果:

在这里插入图片描述

jol依赖中的相关API:

  • ClassLayout:public class ClassLayout:存储了java对象的布局信息
    • parseInstance方法:public static ClassLayout parseInstance(Object instance, Layouter layouter):解析java对象的布局信息,生成ClassLayout对象
    • toPrintable方法:public String toPrintable():生成存储了布局信息的字符串

解析对象的内存布局

一个java对象在内存中包含三个部分:对象头、实例数据、对齐字节。

  • 对象头:存储对象的属性
  • 实例数据:也就是类中的成员变量,如果一个类有父类,实例数据也包含父类的成员变量。
  • 对齐字节:JVM要求java对象占的内存大小应该是CPU字长的倍数,所以后面有几个字节用于把对象的大小补齐至8的倍数

对象头

对象头的组成部分:对象头由mark word、类指针组成,如果对象是一个数组对象,还会有一个数组长度字段。

  • mark word:存储对象运行时的数据,如哈希值、gc分代年龄等,mark word中的数据会在运行时进行修改。mark word的长度是jvm一个字长的大小,也就是说32位JVM的Mark word为32位,64位JVM的Mark word为64位。

  • 类指针:class pointer,指向类对象的指针,jvm通过这个指针确定对象是哪个类的实例

  • 数组长度:array length,如果对象是一个数组,对象头还需要存储数组的长度

mark word

mark word是对象头中最复杂的地方,它存储的数据在运行时会被修改,例如,锁标志位、对象分代年龄、哈希值等,相比之下,类指针和数组长度都是常量,在运行时不会改变。

mark word中存储的数据:(截了两张网上的图)

  • 在64位的虚拟机上:

在这里插入图片描述

  • 在32位的虚拟机上:

在这里插入图片描述

mark word中存储的数据:不考虑对象的状态,只描述对象中存储的数据、数据的长度、数据的位置 (这里是基于64位的虚拟机)

  • 锁标志位:2bit,存放在第一个字节的最后两位,01表示无锁,00表示轻量级锁,10表示重量级锁,11是gc标记
  • 偏向锁标识:1bit,biased_lock,存放在lock之后,只占一个二进制位,为1时表示对象启用偏向锁,为0时表示对象没有偏向锁
  • 对象分代年龄:4bit,存储在biased_lock之后,在gc中,如果对象在survivor区复制一次,年龄增加1,当对象达到设定的阈值时,将会晋升到老年代
  • 哈希值:31bit,调用继承自Object类的hashCode方法后,生成的哈希值会存储到对象头中
  • 指向持有偏向锁的线程的指针:54bit,threadId
  • 偏向时间戳:2bit,epoch,记录线程获取了几次偏向锁
  • 在轻量级锁的状态下指向栈中锁的记录的指针:ptr_to_lock_record:62bit
  • 在重量级锁的状态下指向监视器(管程monitor)的指针:ptr_to_heavyweight_monitor:62bit

为什么偏向锁状态下mark word中存的是线程id,轻量级锁状态下mark word中存的是执行栈中锁记录的指针?如果是轻量级锁,当线程尝试获取锁时,若对象未被锁定,JVM会在该线程的栈帧中创建一个锁记录,并将对象的mark word复制到其中,随后,jvm使用cas操作将mark word更新为指向该锁记录的指针。这么做的好处是什么?支持锁重入:锁记录可以记录重入次数,线程每次重入锁时,锁记录会记录相关信息,释放锁时则减少重入次数,直到完全释放。

类指针

指向类对象的指针,jvm通过这个指针确定对象是哪个类的实例,

数组长度

如果是数组对象,对象头中会存储数组长度

成员变量

存储对象中的成员变量,就是用户可以通过getter、setter方法访问到的数据,要注意,如果一个类有父类,父类中成员变量的值也会被存储在这里。

对象的内存布局-实际案例

案例1:最简单的对象

以入门案例中的Object对象为例:
在这里插入图片描述

打印结果讲解:

  • 第一行:对象的类型,这里是Object类型
  • 第二行到倒数第三行之间是一个表格
    • 表格中的字段:
      • OFFSET:偏移量,对象头中的字段相对于对象头的偏移量,所以第一个字节的偏移量是0
      • SIZE:长度,以字节为单位
      • TYPE DESCRIPTION:类型描述,描述了对象中存储了什么数据
      • VALUE:每个字节中存储的数据分别用十六进制、二进制、和十进制进行了描述
    • 表格中的内容:
      • object header:表示存储的数据是对象头
      • loss due to the next object alignment:为了内存对齐而丢失一部分内存空间
  • 倒数第二行:Instance size,对象大小
  • 倒数第二行:Space losses,丢失的空间,这里为了内存对齐,有4字节的空间没有用。internal表示对象头内丢失的字节,external表示对象头外丢失的字节

要注意:无论是Windows系统,还是Linux系统,都是采用的小端存储法,在低地址存储低位数据。

详细讲解:
在这里插入图片描述

案例2:存储了数据的对象

一个存储了数据的对象:对象中存储了一个String、一个Integer、一个boolean值在这里插入图片描述

案例3:数组对象

一个int类型的数组类型的对象的内存布局,数组的长度是8

在这里插入图片描述

案例4:类对象

一个类对象的内存布局:

在这里插入图片描述

案例5:哈希值在对象头中的存储

在对象的正常状态下,从第二个字节开始,使用31比特位来存储哈希值

代码:

Object obj = new Object();
System.out.println("Integer.toBinaryString(obj.hashCode()) = " 
    + Integer.toBinaryString(obj.hashCode()));
System.out.println(ClassLayout.parseInstance(obj).toPrintable());

执行结果:

在这里插入图片描述

结果解析:

  • 系统采用的是小端存储法,低地址存放的是低位数据
  • 哈希值的长度是31位,从第二个字节开始,到第五个字节的后7位,都是存储的哈希值,第五个字节的第一个比特位没有使用
  • 第五个字节的后7位对应的是哈希值的开头,第四个字节紧随其后,依次类推

解析内存布局时涉及到的知识点

大端法和小端法

大端法和小端法:描述了跨越多个字节的数据在内存中的存储方式

  • 小端法:低地址存放数据低位
  • 大端法:低地址存放数据高位

要注意,数据在存放时,字节是基本存储单元,无论是大端法还是小端法,存放数据的顺序都是从低地址到高地址,区别在于低地址存放低位数据还是高位数据。

Intel处理器采用小端存储法,Windows系统和Linux系统采用的都是Intel处理器。

内存对齐

CPU访问内存时,并不是逐个字节访问,而是以字长(word size)为单位访问,所以数据应该尽可能的放在内存地址是字长的倍数的位置,如果访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅需要一次访问,内存对齐是使用空间换时间,提高了速度。内存对齐是由编译器来完成的

对齐系数:系数是指单项式中的数字因数,对齐系数是编译器做内存对齐时需要使用的常量,每个特定平台上的编译器都有自己的默认"对齐系数",常用平台默认对齐系数:32位系统对齐系数是4,代表4字节,64位系统对齐系数是8

指针压缩

在64位的虚拟机上并且堆内存小于32G时默认开启指针压缩,指向堆内存的指针会由64位被压缩到32位

指针压缩的jvm参数:-XX:+UseCompressedOops // java虚拟机的参数中, + 表示开启, - 表示关闭

案例1:查看虚拟机的参数

在命令行执行命令 java -XX:+PrintCommandLineFlags -version,也可以把这个命令添加到java程序中

打印结果:

-XX:InitialHeapSize=265280448 -XX:MaxHeapSize=4244487168 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops  
-XX:UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_92"
Java(TM) SE Runtime Environment (build 1.8.0_92-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.92-b14, mixed mode)

结果解析:

  • InitialHeapSize:初始化堆内存大小,256M
  • MaxHeapSize:最大堆内存大小,4G
  • PrintCommandLineFlags:打印命令行参数
  • +UseCompressedClassPointers:压缩指向类对象的指针
  • +UseCompressedOops:压缩普通指针,oop,表示ordinary object pointers,普通java对象指针
案例2:关闭指针压缩

为Java程序添加虚拟机参数:-XX:-UseCompressedClassPointers -XX:-UseCompressedOops

查看结果:
在这里插入图片描述

结果解析:对象头占16个字节。因为没有开启指针压缩,锁指向类对象的指针是8个字节

锁升级过程中对象内存布局的变化

偏向锁

创建偏向锁的方式:

  • 第一种:加锁之前先让线程睡5秒。虚拟机在启动的5秒内创建的锁都不是偏向锁,这是虚拟机的一种优化措施
  • 第二种:加上JVM的运行参数,关闭偏向锁的延迟,相关参数:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

案例:

public static void main(String[] args){
    try {
        Thread.sleep(6000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }

    // 程序启动5秒之后再创建对象
    Object LOCK = new Object();

    System.out.println("执行同步代码块之前: \n" + ClassLayout.parseInstance(LOCK).toPrintable());

    synchronized (LOCK) {
        System.out.println("获得锁的线程: \n"
                + ClassLayout.parseInstance(LOCK).toPrintable());
    }

    System.out.println("执行同步代码块之后: \n" + ClassLayout.parseInstance(LOCK).toPrintable());
}

结果解析:

  • 执行同步代码块之前的对象:

在这里插入图片描述

  • 获得锁的线程

在这里插入图片描述

  • 执行同步代码块之后,线程偏向锁不会主动释放偏向锁,打印执行同步代码块之后的内存布局,和获得锁时的内存布局一模一样。

轻量级锁

案例:

private static final Object LOCK = new Object();

public static void main(String[] args) {
    System.out.println("执行同步代码块之前: \n" + ClassLayout.parseInstance(LOCK).toPrintable());
    new Thread(LightWightLockTest::sync).start();
    try {
        Thread.sleep(1000L);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    System.out.println("执行同步代码块之后: \n" + ClassLayout.parseInstance(LOCK).toPrintable());
}

private static void sync() {
    synchronized (LOCK) {
        System.out.println("获得锁的线程: \n" 
                + Thread.currentThread().getName() + ": \n"
                + ClassLayout.parseInstance(LOCK).toPrintable());
    }
}

案例讲解:在同步代码块内打印对象的内存布局,可以看到轻量级锁状态下对象头内的数据

在这里插入图片描述

同步代码块退出后,等1秒钟,再次打印锁对象的内存布局,发现轻量级锁已经变成无锁,指针数据也被清除。

重量级锁

案例:

private static final Object LOCK = new Object();

public static void main(String[] args) {
    System.out.println("执行同步代码块之前: \n" + ClassLayout.parseInstance(LOCK).toPrintable());

    for (int i = 0; i < 2; i++) {
        new Thread(HeavyWeightTest::sync).start();
    }
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }

    System.out.println("执行同步代码块之后: \n"
            + ClassLayout.parseInstance(LOCK).toPrintable());

}

private static void sync() {
    synchronized (LOCK) {
        System.out.println("获得锁的线程: \n"
                + Thread.currentThread().getName() + ": \n"
                + ClassLayout.parseInstance(LOCK).toPrintable());
    }
}

案例讲解:多个线程同时竞争,锁直接由无锁变成重量级锁

在这里插入图片描述

参考

  • https://www.cnblogs.com/mingyueyy/p/13054296.html
  • https://www.cnblogs.com/coderacademy/p/18204403

相关文章:

  • [自动驾驶-传感器融合] 多激光雷达的外参标定
  • 面试基础--MySQL SQL 优化深度解析
  • 新能源汽车工厂如何通过安灯系统实现精益生产
  • [场景题]如何实现购物车
  • 给没有登录认证的web应用添加登录认证(openresty lua实现)
  • PPT小黑第26套
  • Android中的触摸事件是如何传递和处理的
  • 服务器数据恢复—raid5阵列中硬盘掉线导致上层应用不可用的数据恢复案例
  • Linux 文件和目录权限管理详解
  • JavaScript数据结构-Set的使用
  • 宇树科技嵌入式面试题及参考答案(春晚机器人的公司)
  • Idea配置注释模板
  • 什么是安全组及其作用?
  • Zabbix+Deepseek实现AI告警分析(非本地部署大模型版)
  • 【微信小程序】每日心情笔记
  • idea中隐藏目录
  • 深入解析 Nmap 扫描机制的底层原理
  • 海康摄像头接入流媒体服务器实现https域名代理播放
  • Element UI-Select选择器结合树形控件终极版
  • CSS【实战】模拟 html 的 title 属性(鼠标悬浮显示提示文字)
  • 人民日报评论员:党政机关要带头过紧日子
  • 一周观展|一批重量级考古博物馆开馆:从凌家滩看到孙吴大墓
  • 2025吉林市马拉松开跑,用赛道绘制“博物馆之城”动感地图
  • 2025年“新时代网络文明公益广告”征集展示活动在沪启动
  • 中国首艘海洋级智能科考船“同济”号试航成功,可搭载水下遥控机器人
  • 悬疑剧背后的女编剧:创作的差异不在性别,而在经验