【synchronized关键字的原理分析(一)】

Java对象内存布局与锁状态解析
本文详细探讨了Java对象在内存中的布局,包括对象头、实例数据和对齐填充。解释了对象头中的运行时元数据和类型指针,以及如何存储哈希码和锁状态。介绍了小端存储的概念,并通过实例分析了对象头中的MarkWord结构。此外,还讨论了Java锁的三种状态(无锁、偏向锁和重量锁),以及它们在并发性能上的差异。文章使用JOL库展示了对象实例的内存布局,并通过代码示例展示了不同锁状态的转换过程。

我们知道,一个对象在内存中有三部分,对象头,实例数据,对齐填充。
1.对象头包含运行时元数据和类型指针。运行时元数据包含哈希值(这个哈希值就是创建出来的对象在内存中的地址)、GC分代年龄、锁状态标志等。类型指针,指向方法区(元空间)中对象所属的类型。如果创建的是数组,对象头还会保存数组的长度。
2.实例数据。类中定义的成员变量。规则:先放父类的成员变量,在放子类的成员变量。相同宽度的变量被放在一起。如果compactFileds参数为true,子类的窄变量可能插入到父类变量的空隙。
3.对齐填充
虚拟机栈中保存的是一个个栈帧,栈帧中是一个个成员变量,其中cust成员变量指向了堆空间中创建的某个类实例,对象实例包含对象头、实例数据、以及对齐填充。

看下图,对象B没有任何参数时,对象结构如下
在这里插入图片描述
对象头12byte 对齐填充4byte

对象L有一个int类型的参数a时,对象L结构如下
在这里插入图片描述对象头12byte 实例数据4byte

1byte=8bit
仔细看下上图中右边的VALUE,对象头12byte=96位
在这里插入图片描述
第一个word包含了锁的信息,hashcode,gc等信息,第二个word主要指向对象的元数据(对象存储在元空间的地址),它是一个指针。
在这里插入图片描述

发现hashcode存在于对象头,但是为什么顺序是倒着存储,这里不得不提到小端存储。
在这里插入图片描述
什么是小端存储?高字节存在于高地址,低字节存在于低地址。
在这里插入图片描述
图片源自子路。图片上面8byte应该是笔误,int是4个byte=32bit,1对应的就是00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001,从左往后是高字节->低字节,计算机存储方式是小端存储,入栈时高字节在栈的底部,低字节在顶部,出栈时自然就变成00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000,太刁了!
回头再看对象头里面的markword的结构
在这里插入图片描述
前25位没有用到,第二十五位也一定是0,如图所示
在这里插入图片描述
再次验证hashcode确实存在于对象头,可以发现0001011(不是00001011)对应的16进制就是b!
这里再拿hashcode举例,hashcode打印出来是b4c966a,对应的进制0001011(7bit) 01001100 10010110 01101010,从左往后是高字节->低字节,入栈时栈底-栈顶b->4c->96->6a,出栈时就是我们看到的 01101010 10010110 01001100 00001011(补位一个0) 00000000 00000000 00000000,一共56位,前提是计算了hashcode。
再看markword的剩余的8bit,第一位unused,第二位-第五位存储的是对象分代年龄,第六位:偏向标识,是否可偏向。第七八位:锁状态。

回过头再梳理下,new一个对象B,里面没有任何属性或方法时,存储的B是16byte,12byte是对象头,4byte是对齐填充。在B类里面加一个int类型的属性a时,存储的L也是16byte,12byte对象头,4byte实例数据。

再来说说锁的几种状态。主要有三种状态,无锁状态,加锁状态,gc标记状态。那么我们可以理解Java当中获取锁其实就是给对象上锁,也就是改变对象头的状态,如果上锁成功则进入同步代码块。
先说说无锁状态,在不打印hashcode的情况下,此时是无锁可偏向(101 第一个1表示不可偏向 后面01是任何对象初始化后都是01),但未偏向。睡眠5秒是因为jdk1.8默认是4s后开启偏向锁。
在这里插入图片描述
怎么证明此时是可偏向的?加一行加锁的代码。
在这里插入图片描述

再看看打印hashcode的情况下,此时时无锁不可偏向。(001)
在这里插入图片描述
怎么证明此时不可偏向?加一行加锁的代码。
在这里插入图片描述
在有资源竞争(同时竞争一把锁,不是交替执行)的情况下,会变成重量锁。(代码省略)
在这里插入图片描述

性能排序:偏向锁>轻量锁>重量锁
为什么偏向锁性能高?
因为偏向锁是不是走操作系统的,此时首先判断是否可偏向,其次判断是否偏向了,如果未偏向,通过cas将线程id设置到对象头。如果已偏向,取出对象头的线程id和当前线程的id比较,如果相同拿到锁继续执行。从头到尾没有和操作系统交互。

所以如果面试官问你synchronized是不是重量锁?
如果是同一个线程加锁,偏向锁。
如果是不同线程交替执行,轻量锁。
如果是资源竞争抢锁(mutex实现互斥锁),重量锁。

package com.example.并发编程.test;

import com.example.并发编程.entity.B;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
@Slf4j(topic = "example")
public class TestJol {
    //static  B b = new B();
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        final B b = new B();
        System.out.println(Integer.toHexString(b.hashCode()));
        log.debug(ClassLayout.parseInstance(b).toPrintable());

        synchronized (b){
            log.debug(ClassLayout.parseInstance(b).toPrintable());
        }
    }
}

package com.example.并发编程.entity;

/**
 * 对象头
 * 实例数据
 * 对齐填充
 */
public class B {

}
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.10</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>RELEASE</version>
            <optional>true</optional>
        </dependency>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值