写点什么

一文带你了解 Java 并发中的锁优化,让你的代码运行效率翻倍

用户头像
极客good
关注
发布于: 刚刚


结合.java 到.class 字节码文件可以知道,在 jdk8 的情况下,return a+b+c;这句程序实际上底层被转换为 StringBuilder 的追加操作。


每个 StringBuffer.append() 方法中都有一个同步块,锁就是 sb 对象(StringBuilder 新建的一个值为“”的空字符串对象)。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 c


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


oncatString() 方法内部。也就是说,sb 的所有引用永远不会 “逃逸” 道 concatString() 方法之外,其他线程无法访问到它,因此,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了(可以理解为 StringBuilder 虽然线程不安全,但是这里没有关系,因为整个执行过程都在 main 线程中,不会涉及任何线程同步,所以是安全的。这里底层使用 StringBuilder 就可以被认为是一种锁消除)。


锁粗化


===


锁粗化定义:这是一种 JVM 并发优化,如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,这就是锁粗化。


解释:以上面的 return a+b+c;为例,底层被拆分为:



三个 append()追加,如果对每一个 append()都加锁操作,频繁地进行互斥同步操作也会导致不必要的性能损耗,这是 JVM 不乐意看到的,为了提高 JVM 并发性能,此时,JVM 会进行一个优化,就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了,这就是锁粗化。


轻量级锁


====


轻量级锁定义:一种 JVM 并发优化,是指在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。


注意两个:


没有多线程竞争:是指这种轻量级锁只有在没有多线程竞争才能减少性能消耗(实际上,如果有多线程竞争,它的消耗比重量级锁更大,后面会讲)。


传统的重量级锁:是指 synchronized 关键字(和 lock 锁机制)。


引子:HotSpot 虚拟机的对象(对象头部分)的内存布局


HotSpot 虚拟机的对象头(Object Header)分为两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄(Generational GC Age)等,这部分数据是长度在 32 位和 64 位的虚拟机中分别为 32 bit 和 64 bit,官方称它为 “Mark Word”,它是实现轻量级锁和偏向锁的关键。


另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。


对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Work 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。HotSpot 虚拟机对象头 Mark Word,如下表:



问题 1:上表中为什么有两个 01,是不是重复了?


上表的五个项中,第一个表示的是未锁定,其他四个均表示锁定,即第一个 01 表示的是未锁定,后面四个表示的都是锁定.


问题 2:未锁定和偏向锁标记的标记位都是 01,如何区分?


对,因为它们的标记位都是 01,所以根据其他位(biased lock flag 偏向锁标记)区分。


如果还是不懂,一图抵千言:



轻量级锁执行过程:


步骤一:在代码进入同步块的时候,如果此同步对象没有被锁定(如上表所述,锁标志位为 “01” 状态)虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝(官方把这份拷贝加上了一个 Displaced 前缀,即 Displaced Mark Word),这时候线程堆栈与对象头的状态如图:



然后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针。


步骤二( Mark Word 更新为指向 Lock Record 的指针,更新成功): 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位 (Mark Word 的最后 2bit)将转变为 “00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图:



一个完整的轻量级锁获取过程,起码需要经历 4 次 CAS 操作。所以,获取轻量级锁的 CAS 次数总是>=4 的。如果出现线程持续获取锁失败的情况,那么,轻量级锁就会执行膨胀,意思就是升级为重量级锁。且看步骤三。


步骤三(Mark Word 更新为指向 Lock Record 的指针,更新失败):如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果只说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象以及被其他线程线程抢占了。


如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,所标志的状态变为 “10”,Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。这时候线程堆栈与对象头的状态如图:



小结:上面描述的是轻量级锁的加锁过程,它的解锁过程也是通过 CAS 操作来进行的,如果对象的 Mark Word 仍然指向着线程的锁记录,那就用 CAS 操作把对象当前的 Mark Word 和线程中复制的 Displaced Mark Word 替换回来,如果替换成功,整个同步过程就完成了。如果替换失败,说明有其他线程尝试过获取该锁,那就要释放锁的同时,唤醒被挂起的线程。


轻量级锁能提升程序同步性能的依据是 “对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据,也可以说是一种侥幸。


为什么说既是一种经验,又是一种侥幸呢?


如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥量的开销;


如果存在锁竞争,除了互斥量的开销外,还额外发生了 CAS 操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。


通过对轻量级锁的学习,我们知道,轻量级锁不是要取代重量级锁,而是对重量级锁的补充,只有在获取轻量级锁的 CAS 次数,出现线程持续获取锁失败的情况,轻量级锁就会执行膨胀,升级为重量级锁。

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
一文带你了解Java并发中的锁优化,让你的代码运行效率翻倍