Java|java架构之路(多线程)synchronized详解以及锁的膨胀升级过程

Java|java架构之路(多线程)synchronized详解以及锁的膨胀升级过程

文章图片

Java|java架构之路(多线程)synchronized详解以及锁的膨胀升级过程

文章图片

Java|java架构之路(多线程)synchronized详解以及锁的膨胀升级过程

文章图片

Java|java架构之路(多线程)synchronized详解以及锁的膨胀升级过程

文章图片

Java|java架构之路(多线程)synchronized详解以及锁的膨胀升级过程

文章图片

Java|java架构之路(多线程)synchronized详解以及锁的膨胀升级过程

文章图片

Java|java架构之路(多线程)synchronized详解以及锁的膨胀升级过程

文章图片

Java|java架构之路(多线程)synchronized详解以及锁的膨胀升级过程

文章图片

Java|java架构之路(多线程)synchronized详解以及锁的膨胀升级过程

文章图片


前言
synchronized是jvm内部的一把隐式锁 , 一切的加锁和解锁过程是由jvm虚拟机来控制的 , 不需要我们认为的干预 , 我们大致从了解锁 , 到synchronized的使用 , 到锁的膨胀升级过程三个角度来说一下synchronized 。
锁的分类
java中我们听到很多的锁 , 什么显示锁 , 隐式锁 , 公平锁 , 重入锁等等 , 下面我来总结一张图来供大家学习使用 。

这次博客我们主要来说我们的隐示锁 , 就是我们的无锁到重量级锁 。
synchronized的使用
我们先来看一段简单的代码

就这样synchronized就可以使用了 , 这样是每次去拿全局对象的object去锁住后续的代码段 。 我们来看一下汇编指令码

明显看到了两个很重要的方法monitorenter和monitorexit两个方法 , 也就是说我们的synchronized方法加锁是基于monitorenter加锁和monitorexit解锁来操作的

我们得知是由monitorenter来控制加锁和monitorexit解锁的 , 我们完全可以这样来操作 。 上次我们说过一个unsafe类 。

就是我们上次说的unsafe那个类给我们提供了加锁和解锁的方法 , 这样就是实现夸方法的加锁和解锁了 , 但是超级不建议这样的使用 , 后面的AQS回去说别的方式 。 越过虚拟机直接操作底层的 , 我们一般是不建议这样来做的 。
我们还可以将synchronized锁放置在方法上 。 例如

这样加锁是加在了this当前类对象上的 。 如果不加static , 锁是加在类对象上的 , 需要注意我们用的spring的bean作用域
并且我们的synchronized是一个可重入锁 , 在jvm源码中有一个数值来记录加锁和解锁的次数 , 所以我们是可以多次套用synchronized的

synchronized到底锁了什么
还是拿上个每次加锁的时候会在对象头内记录我们的加锁信息 , 我们这里来说一下对象头里面都放置了什么吧 。
以32位JVM内部存储结构为例

由此看出对象一直是有一个位置来记录我们的锁信息的 。 说到这我们就可以来看一下我们锁的膨胀升级过程了 。
锁的膨胀升级
我们说过了对象头的内容 , 接下来可以说说我们的锁内部是如何升级上锁的了 。 从无锁到重量级锁的一个升级过程 , 我们来边画图 , 边详细看一下 。
无锁状态:

开始时应该这样的 , 线程A和线程B要去争抢锁对象 , 但还未开始争抢 , 锁对象的对象头是无锁的状态也就是25bit位存的hashCode , 4bit位存的对象的分代年龄 , 1bit位记录是否为偏向锁 , 2bit位记录状态 , 优先看最后2bit位 , 是01 , 所以说我们的对象可能无锁或者偏向锁状态的 , 继续前移一个位置 , 有1bit专门记录是否为偏向锁的 , 1代表是偏向锁 , 0代表无锁 , 刚刚开始的时候一定是一个无锁的状态 , 这个不需要多做解释 , 系统不同内部bit位存的东西可能有略微差异 , 但关键信息是一致的 。
偏向锁:
这时线程开始占有锁对象 , 比如线程A得到了锁对象 。

就会变成这样的 , 线程A拿到锁对象 , 将我们的偏向锁标志位改为1 , 并且将原有的hashCode的位置变为23bit位存放线程A的线程ID(用CAS算法得到的线程A的ID) , 2bit位存epoch , 偏向锁是永远不会被释放的 。
接下来 , 线程B也开始运行 , 线程B也希望得到这把锁啊 , 于是线程B会检查23bit位存的是不是自己的线程ID , 因为被线程A已经持有了 , 一定锁的23bit位一定不是线程B的线程ID了
【Java|java架构之路(多线程)synchronized详解以及锁的膨胀升级过程】
然后线程B也会不甘示弱啊 , 会尝试修改一次23bit位的对象头存储 , 如果说这时恰好线程A释放了锁 , 可以修改成功 , 然后线程B就可以持有该偏向锁了 。 如果修改失败 , 开始升级锁 。 自己无法修改 , 线程B只能找“大哥”了 , 线程B会通知虚拟机撤销偏向锁 , 然后虚拟机会撤销偏向锁 , 并告知线程A到达安全点进行等待 。 线程A到达了安全点 , 会再次判断线程是否已经退出了同步块 , 如果退出了 , 将23bit位置空 , 这时锁不需要升级 , 线程B可以直接进行使用了 , 还是将23bit的null改为线程B的线程ID就可以了 。

轻量级锁:
如果线程B没有拿到锁 , 我们就会升级到轻量级锁 , 首先会在线程A和线程B都开辟一块LockRecord空间 , 然后把锁对象复制一份到自己的LockRecord空间下 , 并且开辟一块owner空间留作执行锁使用 , 并且锁对象的前30bit位合并 , 等待线程A和线程B来修改指向自己的线程 , 假如线程A修改成功 , 则锁对象头的前30bit位会存线程A的LockRecord的内存地址 , 并且线程A的owner也会存一份锁对象的内存地址 , 形成一个双向指向的形式 。 而线程B修改失败 , 则进入一个自旋状态 , 就是持续来修改锁对象 。

重量级锁:
如果说线程B多次自旋以后还是迟迟没有拿到锁 , 他会继续上告 , 告知虚拟机 , 我多次自旋还是没有拿到锁 , 这时我们的线程B会由用户态切换到内核态 , 申请一个互斥量 , 并且将锁对象的前30bit指向我们的互斥量地址 , 并且进入睡眠状态 , 然后我们的线程A继续运行知道完成时 , 当线程A想要释放锁资源时 , 发现原来锁的前30bit位并不是指向自己了 , 这时线程A释放锁 , 并且去唤醒那些处于睡眠状态的线程 , 锁升级到重量级锁 。

逃逸分析
很简单的一个问题 , 实例对象存在哪里?到底是堆还是栈?问题我先不回答 , 我们先看一段代码 。

就是我们运行一个创建对象的方法 , 一次性创建50万个Car对象 , 然后我们让我们的线程进行深度的睡眠 , 两个打印是为了知道我们的对象已经开始创建了和已经创建完成了 。 我们来运行一下 。

然后运行jmap -histo命令来查看我们的线程

我们可以看到 , car对象并没有产生50万个 , 别说会被GC掉对象 , 在运行之前我已经加了GC日志的参数-XX:+PrintGCDetails , 控制台没有打印任何GC日志的 。 那么为什么会这样呢?我们来看一下我们的代码 , 由createCar代码创建了car对象 , 但car对象并没有被其它的方法或者线程去调用 , 虚拟机会认为你这对象可能只是一个实例化 , 并没有进行使用 , 这时虚拟机会给予你一个优化 , 就是对于可能没有使用的对象进行一次逃逸 , 也就是我们说到的逃逸分析 。 我们加入 -XX:-DoEscapeAnalysis参数再看一次 。

这也就是关闭了我们的逃逸分析 , 虚拟机就会真的为我们创建了50万个对象 。 也就是说开启了逃逸分析有一部分对象只是创建了线程栈上 , 当线程栈结束 , 对象也被销毁 , 上面的问题也就有答案了 , 实例对象可能存在堆上 , 也可能存在栈上 。


    推荐阅读