java锁的膨胀过程-synchronized(1)

Question

要说到java中的锁,我相信大家都会能言善道,各持己见,滔滔不绝。但是everyone都明白,口吐芬芳不影响造火箭,但是刺啦必定漏风,所以搞定某一样东西并不是杠,而是技术人的一种情怀,您瞧:

  1. 如何证明锁的存在,证据。
  2. 锁的种类不止一种,特点。
  3. 锁的膨胀,过程。
切入点

先说一点概念性的东西,HotSpot虚拟机中,对象在内存中主要包括三块区域:对象头、实例数据和对齐填充。
一个java对象会存储我们常说的堆(Heap)当中,他是以什么的状态或者说有怎样的表现形式?
openjdk提供了jar包,可以查看对象头信息:

<dependency>
	<groupId>org.openjdk.jol</groupId>
 	<artifactId>jol‐core</artifactId>
 	<version>0.9</version>
</dependency>

在玩他(‘她’)之前,请看:

//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
//

在openjdk源码openjdk\hotspot\src\share\vm\oops目录下的markOop.hpp文件当中注释,我用可视化图形(个人创作)给大家解释一下(这是主图,大家要记住,下文我会多次提及主图):
在这里插入图片描述

我知道大家有疑问,自上而下,每一行每一行的挨个来。

object header

在这里插入图片描述

openjdk官方声明了:用我0.5级蹩脚的英文水平给大家翻译一下,每个堆对象包括有关对象的布局,类型,GC状态,同步状态和hashcode标识的几种基本信息,是开头的一种公共结构。
(每个oop都指向一个对象标头。) 由两个词组成。 也就是mark word和klass pointer。

object header占128 bits,mark word 64 bits,JVM默认会开启klass pointer指针压缩,将64bit压缩到32bit,这里不做重点解释。

mark word

每个对象标头的第一个单词称为mark word,用我个人理解告诉大家,不敢与各路大神苟同,mark word是一组bit域,包括hashcode值,偏向线程信息,偏向时间戳,分代年龄,可偏向位,锁状态标识。不同的锁状态会对应不同的一组bit域,大家可能会有好多疑问,先记住,重要的几点之后会做详细解释。

klass word

klass word所谓概念就是指向我们元数据指针,貌似不懂,没关系,笔者告诉大家:一个类会被JVM加载到内存当中,也就是方法区会存在Class文件模板,指针就是指向这个模板。

实例-无锁状态

我个人比较注重wirte less do more。

//指定一个pojo类
public class James {
//添加一个boolean类型属性
    private boolean mvp;
}
public class Laker {
    public static void main(String[] args) {
        James james = new James();
        //查看jvm基本信息
        out.println(VM.current().details());
  	 //对象头基本信息
        out.println(ClassLayout.parseInstance(james).toPrintable());
    }
}

首先看jvm打印信息:
在这里插入图片描述
对象信息左半部分(有点长,分开吧):
在这里插入图片描述
右半部分:
在这里插入图片描述
这部分信息量有点大,请大家耐心。

有数据的前两行,也就是我们说的mark word,对应主图,理论上会有(25+31)bit hashcode值,为什么没有,很显然没有计算嘛!

public class Warriors {

    public static void main(String[] args) {
        James james = new James();
//        out.println("before hash");
//        out.println(ClassLayout.parseInstance(james).toPrintable());
         //JVM 计算的hashcode,16进制
        out.println("jvm‐‐‐‐‐‐‐‐0x"+Integer.toHexString(james.hashCode()));
        out.println("after hash");
        out.println(ClassLayout.parseInstance(james).toPrintable());
    }
}

大家可以参考我的代码去测试,测试结果:
在这里插入图片描述
在这里插入图片描述

上引用到此下引用为高能部分,通过jvm hashcode计算你会发现有值了,也就是0x66a29884(16进制),如果按照上图8->7->6->5…倒序二进制转十六进制计算你会发现,结果0x018498a266,这TIMI什么鬼,先不要着急,如果按照1->2->…顺序计算,意外惊喜出现了,拼出了66a29884,为什么?
在这里大家可以自行百度,但是我还是想解释一番,牵扯到CPU大小端模式,所谓的小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,也就是说十六进制84会对应数字7对应的10000100二进制数值,以此类推。赋图:
在这里插入图片描述
大家先记着,如果计算了hashcode必然不会存在偏向状态!

总结:1->2->3->4->5->6->7,每个阿拉伯数字对应一组8bit二进制数值,所以7*8bit=56bit,追主图无锁状态栏细看,是不是对应前56位(25unused+31hashcode)。
我觉得大家看到这已经很不容易了,帮大家加把劲。有仁兄就会想:我看到这了,这TIMI跟锁有什么关系,就像大家爱看的光碟,带着激动的心情看了半小时我坤哥运球。有兄弟可能明白了我的心意, 跬步千里 ,厚积薄发!
上文提到对象头64bits,hashcode夺去56个,剩余8个呢,大家有没有想到上图64位mark word 我特意圈出来56位,所以圈以外的那8位(00000001)能让你想到什么?
直接上图:
在这里插入图片描述
从上到下,大家没有感受到一点锁的气息,好,现在来了,这8位,前5位都是0,目前不做研究,我们只关心后3位。
001,偏向状态为0,对象状态为01,说明了一个问题:一个对象在不加锁的情况下mark word是56hashcode+1unused+4GC+1偏向状态+2对象状态。
自此主图第3行解释完毕。

实例-偏向锁

001搞定了,换个状态搞搞吧!
上代码:

public class Net {

    public static void main(String[] args) {
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        James james = new James();
        out.println("if lock");
//        james.hashCode();
        out.println(ClassLayout.parseInstance(james).toPrintable());

    }
}

比较无锁状态代码图,大家看出有什么区别,是不是休眠了5s,大家可以回头看一下无锁状态代码,包括对象头信息,然后来对比一下:
在这里插入图片描述
状态变为 101 ,偏向状态是不是由0变化为1了,这就是偏向锁,大家会疑问,为什么休眠5s就会偏向,hashcode哪去了,前56bit怎么全是0,继续看:

public class Net {

    public static void main(String[] args) {
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        James james = new James();
        out.println("no lock");
        james.hashCode();
        out.println(ClassLayout.parseInstance(james).toPrintable());

    }
}

我让他计算了一下hashcode,大家再看:
在这里插入图片描述
状态变为 001 ,这个状态码大家熟悉,无锁状态,即使我休眠5s,他也不会是偏向。结论:因为既生hash何生偏向,所以某个对象计算hashcode之后无法偏向。
有的兄弟就会疑问,为什么中间只差两只小白兔顺序执行时间就会开启偏向锁,你代码中也没有加锁啊?

我给大家高能一波:先考虑一个问题,当你点击main启动,只是启动你程序而已吗?no_no_no,,这当中是不是还包含了启动jvm虚拟机,启动gc线程等N多事情,其中包含延迟偏向锁行为,为什么要延迟偏向锁,因为偏向锁会消除锁,而消除锁是非常复杂的一件事情。我个人学习理解,大家可以自行思考,假设不延迟偏向,jvm启动中会出现多个同步synchronized(foo),拿其中一个同步讲,第一个线程访问,偏向锁,第二个线程来访问就会锁升级,存在一个非常复杂的过程,可以说大部分的同步块都会进行资源竞争。jvm启动,包括各种gc启动会有大量同步,作者介于性能考虑,不能每个同步都进行一次锁升级,所以规定4s之后开始存在偏向。

public class Net {
    public static void main(String[] args) {
//        try {
//            TimeUnit.SECONDS.sleep(5);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
        James james = new James();
        out.println("if lock");
//        james.hashCode();
        synchronized (james) {
            out.println(ClassLayout.parseInstance(james).toPrintable());
        }

    }
}

我把睡眠时间注释,加上 -XX:BiasedLockingStartupDelay=0 参数,意思是取消偏向锁延迟,跟睡眠5s效果是一样的,不同的是我添加一个同步代码块,这次再看一下对象头,下图:
在这里插入图片描述
依然是偏向锁,结论:关闭偏向锁延迟,两次测试结果,统一main线程,同一对象,针对mark word,无论加不加同步,都是偏向锁。

这个结论是非常错误的,我接下来就要证明:

public class Bucks {
    public static void main(String[] args) {
        James james = new James();
        out.println("start biased without sync");
        out.println(ClassLayout.parseInstance(james).toPrintable());

        synchronized (james){
            out.println("start biased sync");
            out.println(ClassLayout.parseInstance(james).toPrintable());
        }
    }
}

在这里插入图片描述
同样关闭偏向延迟,加了同步和不加同步怎么值不一样,为什么?

两次打印偏向状态+对象状态都是 101,却是不同的概念,
大家回过头看主图,作为偏向锁,bit域是怎样的:54bit(threadinfo)+2bit(epoch)+1bit(unused)+4bit(GC)+101,
就是说第二组打印标红部分为54bit线程信息+2bit偏向时间戳,
也就证明此锁为偏向锁。
大家会问,那第一组,状态既是101,而且又没有线程信息,那是什么?
这代表无锁可偏向状态,不是偏向锁,如果在代码打印信息之前,对象调用hashcode方法,就会出现 56hashcode+1unused+4GC+001的mark word,无锁状态
在这里插入图片描述
在这里插入图片描述

也再次证明,对象调用hashcode方法之后,再无偏向状态!

篇幅略长,只是synchronized一个小开头,后面笔者会实例证明轻量锁,重量锁的存在以及他们的特点和性能差异,还有锁的膨胀过程。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值