8.对象与锁标记

synchronize既然能实现互斥,那其内部是如何做的呢?我们逐步分析。

问题1:根据前面的说明,要实现一个互斥的锁,存在多个线程同时抢占同一个资源的情况,此时如何允许只有一个线程得到资源,而其他线程只能等待呢?等待的线程应该做什么呢?

问题2:等待的线程此时什么都做不了,因此需要让其释放CPU资源,避免浪费,该如何实现呢?假如抢资源的线程很多,我们该如何管理这些等待的线程呢?

第三个问题:如果多个线程一次只能有一个获得锁,效率可能会很低。例如高考发榜的时候,每个考生都想知道自己的成绩, 但是假如只能一个一个的看,那效率太低,事实上这时候是可以允许多个人同时看的,又该如何做呢?

第一个问题:多个线程抢一个资源,虽然是同时, 但是仍然有一定的时间间隙的,我们可以这么做:一个资源被抢到之后,可以在其上做个标记,然后其他线程读的时候发现已经被其他线程标记了,就不再访问。这就像生活中常见的“占座”,而抢到的线程处理完之后再将该标记恢复了。由此就可以一次只允许一个线程得到资源。

想一下,生活中,我们如何在餐厅 教室占位子的

那该在哪里加标记呢?很明显,如果是给对象加锁,那就修改对象的标记,这就是我们后面要介绍的内容。

第二个问题,JVM并没处理该情况,只是通过UnSafe等类将任务转给了操作系统,因此我们不过多讨论。

第三个问题:这就涉及到如何优化锁了,我们可以将锁设计为多种类型,根据不同的场景来调整锁,这就是无锁、偏向锁、轻量级锁、重量级锁几种类型,他们是怎么回事,又是如何工作的呢?这也是我们后面要介绍的内容。

本节我们就解决一个问题:两个线程竞争一个资源时,需要加锁, 这个锁到底是什么?各个线程又是如何知道锁的状态是空闲还是繁忙的?

如果针对堆内存中的对象加锁,一定是将JVM的对象做了某种处理,因此要解决我们的疑问,需要先看一下对象的结构。

Java对象结构可以分为三个部分:对象头、实例数据、对其填充。当我们构建一个Object object=new Object()对象实例时,这个object实例的最终存储结构如下:

接下来我们就详细分析每个部分的结构和作用。

 对象头

在上面图示中,我们也看到,Java的对象头由三个部分组成:Mark Word、Klass Pointer和Length。

Mark Word记录了对象和锁相关的信息,当这个对象作为锁对象来实现synchronized的同不操作时,锁标记和相关信息都存储在Mark Word中,具体如下:

 在32位系统中Mark Word的长度是4字节。而在64位系统中 ,则为8字节,因此结构也有所不同

这些标记位具体是怎么工作的,这里解释一下上面的几个标志位,

  • 锁标志位(lock),用于区分锁状态,11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。

  • 偏向锁标记位,biased_lock,表示是否偏向锁,由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。

  • 分代年龄(age),表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。

  • 对象的hashcode(hash) 运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁,hashcode会被转移到Monitor中。

  • 偏向锁的线程ID(JavaThread): 偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。

  • epoch 偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。

  • ptr_to_lock_record 轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针。

  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针。

Klass Pointer 表示指向类的指针,JVM通过这个指针来确定对象具体是哪个类的实例。我们根据JVM知识可以知道,这个指针是指向方法区的。其长度由JVM的位数决定,在32位和64位下分别为4个字节和8个字节 ,不过在JDK8中,开启指针压缩之后,64位系统中也可以只占4个字节。

Length表示数组的长度,这个只有构建对象数组时才会有。

对象头之外的部分 ,实例数据就是类中所有成员变量组成的整体,比如一个对象中包含int、布尔、long等类型的成员变量,这些成员变量就存储在实例数据中。

对其填充也在很多场景下都遇到,对象的长度一般不可能正好是8字节的整数倍, 但是对于计算机而言,将每个对象都设置为8字节的整数倍更利于管理,因此假如某个对象的大小不是8的整数倍时就将其填充成整数倍。

通过ClassLayout查看对象布局

我们接下来通过一个例子来实际看一下对象是怎么布局,为此我们需要借助OpenJdk提供的工具jol-core。

我们可以通过pom坐标导入 ,也可以下载jar包直接导入,成功之后写如下代码:

import org.openjdk.jol.info.ClassLayout;
public class ClassLayoutExample {
    public static void main(String[] args) {
        ClassLayoutExample example = new ClassLayoutExample();
        System.out.println(ClassLayout.parseInstance(example).toPrintable());
    }
}

然后执行,就直接打印出来了:

 上面Offset是偏移量,单位是字节。size表示大小,value则对应内存中的值,我们可以看到大小是12字节。其中前8个是对象头,之后后4个字节表示类型指针。很明显12不是8的倍数 ,所以后面又填充了四个字节,一共16个字节。

在64 位 JVM中,由于对象的指针变大,为了减少内存占用,可以压缩 OOP 这就是为什么对象头只有12个字节。我们可以通过给JVM添加如下参数来防止内存压缩:

-XX:-UseCompressedOops

此时再执行就是:

可以看到此时对象头大小就是16字节,就不需要4个字节的填充了。

思考也许有人会有疑问,既然不填充是16字节,压缩之后是12字节,但是还要给补4个字节,是不是多余了?这个不是的,因为填充几乎不占用空间。

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
1. 目录 1. 2. 目录 .........................................................................................................................................................1 JVM ...................................................................面试.................................................................................... 19 2.1. 线程 ...................................................................................................................................................... 20 2.2. JVM 内存区域 ..................................................................................................................................... 21 2.2.1. 程序计数器(线程私有) ................................................................................................................ 22 2.2.2. 虚拟机栈(线程私有) .................................................................................................................... 22 2.2.3. 本地方法区(线程私有) ................................................................................................................ 23 2.2.4. 堆(Heap-线程共享)-运行时数据区 ...................................................................................... 23 2.2.5. 方法区/永久代(线程共享) ..................................................................................................... 23 2.3. JVM 运行时内存 ................................................................................................................................. 24 2.3.1. 新生代 .......................................................................................................................................... 24 2.3.1.1. 2.3.1.2. 2.3.1.3. 2.3.1.4. Eden 区 .................................................................................................................................................... 24 ServivorFrom........................................................................................................................................... 24 ServivorTo ...........................................

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

纵横千里,捭阖四方

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值