Hotspot 偏向锁的评估与改进(六)

230 阅读6分钟

第三章

3.4 偏向锁

基于之前章节介绍的轻量级和重量级锁机制,Hotspot支持无存储的批量重偏向和撤销. 通过JVM参数能够切换该功能,而且它是默认开启的.Mark word使用一个二进制位来表示是否已经偏向该对象或禁止偏向该对象. 这就是图3.1和3.2中展示的第三低位. 如果该位为0,那么对象处于无锁并且不允许为该对象加偏向锁. 如果为1,那么锁可以为一下状态之一:

  • 匿名偏向: 线程指针为NULL(0)意味着没有线程偏向该锁. 第一个获取该锁的线程注意到此值后使用原子性的CAS指令将锁偏向自己. 这是允许偏向的对象初始状态.
  • 重偏向 偏向锁的epoch字段无效(不匹配当前类的epoch). 下一个线程会注意到此值并使用原子性的CAS指令将锁偏向自己. 批量重偏向期间,未被持有的偏向锁设置为此状态来以允许快速重偏向
  • 已偏向: 线程指针不为NULL并且epoch有效, 这意味着某一线程正持有该锁.

由于偏向锁使用hashcode字段作为偏向线程的标识, 所以偏向锁无法用于已哈希对象. 在允许偏向的对象上计算哈希时会首先撤销偏向(不管有效或无效),然后使用CAS将计算的哈希码放置到mark word中. 然而这仅适用于标识哈希(identify hash), 即对象的hashcode()方法计算的结果. 重写hashcode()的类进行哈希计算时无需标识哈希(当然,除非显式地使用对象的哈希码), 可以仍然使用偏向锁机制.

Hotspot在每一个已加载类的类元中存储原型(prototype)mark word, 是否允许该类偏向由原型中的偏向标志位决定. 同样,该类的当前epoch也保存在这个原型中. 这意味着新对象无需进一步修改就可简单拷贝该原型. 批量重偏向期间原型的epoch会被更新, 批量撤销会改变原型为无偏向锁状态(设置偏向位为0).

通过CAS原子指令插入线程指针到mark word中来获取偏向锁. 自然,使锁偏向的先决条件是它要嘛是匿名偏向要嘛是可重偏向(某一时刻一个锁只能偏向一个线程). 一旦锁偏向,递归的加锁和解锁只需读取对象头和类原型来确认锁是否未被撤销. 偏向锁的锁记录中保存了指向锁对象的指针,但它并未被初始化.图3.5可看到偏向锁的示例. 未使用的displaced mark word内存地址仍被需要以防偏向锁撤销,在此期间锁被转换成轻量级锁.

在Hotspot中使用全局安全点来撤销锁. 撤销期间,撤销者(revoker)遍历偏向锁持有线程的锁记录来推断对象是否当前被锁定. 如果在偏向线程中发现该锁, 锁记录被修改就像使用了轻量级锁. 如果锁当前未被持有,那么依赖于因何导致撤销, 对象或被禁止使用偏向锁或重偏向了撤销线程.

尽管偏向锁被启用了,但由于在JVM启动时就使用偏向锁带来的低性能,该功能在启动的前4秒仍然是不可用的. 这意味着原型mark word在此期间它们的偏向锁标志位为0, 禁止实例对象的偏向. 四秒后,所有原型对象的标志位被设置为1, 因此允许偏向新对象

GC期间,对象mark word以原型mark word为规范. 这减少了GC期间必须要保留的对象头数量. 那些已计算哈希,被锁定或本地禁止偏向(原型偏向标志位为1,而实际偏向标志位为0)的对象头在GC期间将被保留, 而那些已偏向但当前并不是被偏向线程持有的对象头则不然. 这意味着未被锁定的偏向对象可能在每次GC时忘记偏向(依赖于GC),强制将这些锁设置为匿名偏向状态.[12]

译者注 [12] 这一段的原文很拗口,而且理解起来也很费劲,下来再好好消化消化

在启用偏向的情况下获取锁时,将采取以下步骤:

  1. 测试对象的偏向标志位 如果是0, 那么锁未偏向, 并且将使用轻量级锁算法

2.测试类的偏向标志位

检查对象所属类的原型mark word中偏向标志位的设置, 如果设置为0,那么该类所有对象将被全部禁止偏向,并且对象的偏向标志位应被重置以及被轻量级锁替换.

  1. 验证epoch

检查对象mark word中的epoch是否匹配原型mark word. 如果不匹配, 那么偏向已失效并被重偏向. 此时锁定线程可以使用原子CAS指令来轻松尝试重偏向.

  1. 检查拥有者

将偏向线程标识与锁定标识比较. 如果匹配, 当前锁定线程持有该锁并可安全退出. 如果不匹配,锁被看作匿名偏向,锁定线程使用原子CAS指令来尝试获取偏向锁. 如果失败, 撤销偏向(可能会引入安全点)并退回到轻量级锁, 一旦成功,那么加锁线程成为偏向锁的拥有者并退出

初始检查完成后,可通过一条条件跳转指令完成后三步的操作,在第一次加载原型mark word,使用锁定线程的标识与其按位或然后使用对象mark word与上述结果异或(XOR). 如果结果是0,意味着该类可以偏向, epoch有效并且锁定线程就是当前锁的持有者. 否则, 如果结果非0, locking thread must investigate which of the bits that differed to know which of the three cases from above that was encountered to be able to proceed. [14]

译者注 [14] 这一段真的很难翻译,容我再消化消化