【JVM 之锁优化笔记】
作者:互联网
文章目录
高效并发是从JDK 5升级到JDK 6后一项重要的改进项,HotSpot虚拟机开发团队在这个版本上花 费了大量的资源去实现各种锁优化技术,如 适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁膨胀(Lock Coarsening)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)等,这些技术都是为了在线程之间更高效地共享数据及解决竞争问题,从而提高程序的执行 效率。
自旋锁与自适应自旋
自旋锁可以设置等待时间和自旋次数,但是无论是默认值还是用户指定的自旋次数,对整个Java虚拟机中所有的锁来说都是相同的。所以在 JDK 6中对自旋锁的优化,引入了自适应的自旋
。所谓的自适应自旋锁就是:以历史数据来作为参考,进而判断自旋时间与次数。如果在同一个锁对象上,自旋等待刚 刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成 功,进而允许自旋等待持续相对更长的时间,比如持续100次忙循环。另一方面,如果对于某个锁,自旋很少成功获得过锁,那在以后要获取这个锁时将有可能直接省略掉自旋过程,以避免浪费处理器资 源。有了自适应自旋,随着程序运行时间的增长及性能监控信息的不断完善,虚拟机对程序锁的状况 预测就会越来越精准,虚拟机就会变得越来越“聪明”了。
锁的消除(等到 逃逸分析技术 搞明白后来写)
指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享 数据竞争的锁进行消除
。锁消除的主要判定依据来源于逃逸分析的数据支持(第11章已经讲解过逃逸 分析技术),如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可 以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须再进行。
锁粗化
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据 的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变少,即使存在锁竞争,等 待锁的线程也能尽可能快地拿到锁。
大多数情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗
。因此在出现这种情况之后,编译器会帮我们进行锁粒度的粗化!
轻量级锁
轻量级锁是JDK 6时加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实 现的传统锁而言的,因此传统的锁机制就被称为“重量级”锁。不过,需要强调一点,轻量级锁并不是用来代替重量级锁的,它设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗
。
对象头的设计思路
对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到Java虚拟机的空间使用效 率,Mark Word被设计成一个非固定的动态数据结构,以便在极小的空间内存储尽量多的信息。它会 根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机中,对象未被锁定的状态下, Mark Word的32个比特空间里的25个比特将用于存储对象哈希码,4个比特用于存储对象分代年龄,2 个比特用于存储锁标志位,还有1个比特固定为0(这表示未进入偏向模式)。对象除了未被锁定的正 常状态外,还有轻量级锁定、重量级锁定、GC标记、可偏向等几种不同状态,这些状态下对象头的存 储内容如表13-1所示
。
CAS 见:面试必问的CAS,你懂了吗?
轻量级锁的加锁工作流程
- 在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“ 01”状态),虚拟机首先
将在当前线程的栈 帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝
(官方为这份拷贝加了一个Displaced前缀,即Displaced Mark Word),这时候线程堆栈与对象头的状态如图13- 3所示。
- 然后,
虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针
。如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位(Mark Word的 最后两个比特)将转变为“ 00”,表示此对象处于轻量级锁定状态。这时候线程堆栈与对象头的状态如 图13-4所示。
- 如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了,否则就说明这个锁对象已经被其他线程抢占了。
如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志 的状态值变为“10”,此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。
解锁过程也同样是通过CAS操作来进行的,如果对象的 Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来。假如能够成功替换,那整个同步过程就顺利完成了;如果替换失败,则说明有 其他线程尝试过获取该锁,就要在释放锁的同时,唤醒被挂起的线程(不太明白这是为什么?)
。
偏向锁
偏向锁是JDK 6中引入的一项锁优化措施,它的目的是消除数据在无竞争情况下的同步原语, 进一步提高程序的运行性能
。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。
基础思路就是:这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需 要再进行同步。
偏向锁的加锁流程
- 当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为“01”、把偏向模式设置为“1”,表示进入偏向模式。同时使用CAS操作把获取到这个锁的线程 的ID记录在对象的Mark Word之中。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁及对Mark Word的更新操作 等)。
- 一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束。根据锁对象目前是 否处于被锁定的状态决定是否撤销偏向(偏向模式设置为“ 0”),撤销后标志位恢复到未锁定(标志位 为“ 01”)或轻量级锁定(标志位为“ 00”)的状态,后续的同步操作就按照上面介绍的轻量级锁那样去 执行。偏向锁、轻量级锁的状态转化及对象Mark Word的关系如图13-5所示。
偏向锁可以提高带有同步但无竞争的程序性能,但它同样是一个带有效益权衡(Trade Off)性质 的优化,也就是说它并非总是对程序运行有利。如果程序中大多数的锁都总是被多个不同的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下,有时候使用参数-XX:- UseBiasedLocking来禁止偏向锁优化反而可以提升性能。
QA
- 为什么必须锁会膨胀?且必须膨胀?(TODO:待回答)
- 轻量级锁能提升程序同步性能的依据是“
对于绝大部分的锁,在整个同步周期内都是不存在竞争的
”这一经验法则。如果没有竞争,轻量级锁便通过CAS操作成功避免了使用互斥量的开销;但如果确 实存在锁竞争,除了互斥量的本身开销外,还额外发生了CAS操作的开销。因此在有竞争的情况下, 轻量级锁反而会比传统的重量级锁更慢。
标签:之锁,Word,对象,笔记,Mark,线程,轻量级,JVM,偏向 来源: https://blog.csdn.net/liushengxi_root/article/details/122741785