原因:当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁。这个过程是要消耗一定的成本的,所以如果说运行时的场景本身存在多线程竞争的,那偏向锁的存在不仅不能提高性能,而且会导致性能下降。因此,JVM中增加了一种批量重偏向/撤销的机制。

批量重偏向:

当一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,会导偏向锁重偏向的操作。

对应的jdk源码属性设置
intx BiasedLockingBulkRebiasThreshold = 20 默认偏向锁批量重偏向阈值

批量撤销:

在多线程竞争剧烈的情况下,使用偏向锁将会降低效率,于是乎产生了批量撤销机制。

对应的jdk源码属性设置
intx BiasedLockingBulkRevokeThreshold = 40 默认偏向锁批量撤销阈值

发送批量重偏向和批量撤销

以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。

当达到重偏向阈值后,假设该class计数器继续增长,当其(class的epoch值)达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

epoch在批量重偏向的使用过程

  1. 首先引入一个概念epoch,其本质是一个时间戳,代表了偏向锁的有效性,epoch存储在可偏向对象的MarkWord中。除了对象中的epoch,对象所属的类class信息中,也会保存一个epoch值。
  2. 每当遇到一个全局安全点时(这里的意思是说批量重偏向没有完全替代了全局安全点,全局安全点是一直存在的),比如要对class C 进行批量再偏向,则首先对 class C中保存的epoch进行增加操作,得到一个新的epoch_new
  3. 然后扫描所有持有 class C 实例的线程栈,根据线程栈的信息判断出该线程是否锁定了该对象,仅将epoch_new的值赋给被锁定的对象中,也就是现在偏向锁还在被使用的对象才会被赋值epoch_new。
  4. 退出安全点后,当有线程需要尝试获取偏向锁时,直接检查 class C 中存储的 epoch 值是否与目标对象中存储的 epoch 值相等,
    1. 如果epoch 值不相等,则说明该对象的偏向锁已经无效了,此时竞争线程可以尝试对此对象重新进行偏向操作(因为(3)步骤里面已经说了只有偏向锁还在被使用的对象才会有epoch_new,这里不相等的原因是class C里面的epoch值是epoch_new,而当前对象的epoch里面的值还是epoch)
    2. 如果epoch 值相等,则说明该对象的偏向锁有效,那么就会将偏向锁撤销变为无锁状态,通过CAS变为轻量级锁。

**

对比未到达阈值,同步时候的判断

  1. 当第一个线程创建了大量对象并执行了初始的同步操作,后来第二个线程也来将这些对象作为锁对象进行操作,每次同步 class的对象,发生对象撤销,epoch会加1, 同步的对象的epoch是默认值不变的。
  2. 当到class中epoch达到阈值20时候,会发生批量重偏向,这时候会同步所有的线程所占有的对象的epoch值。但是当class的epoch值达到阈值40时候,第三个线程(非第二个线程,可以是第一个线程)就会发生批量撤销。

  3. 没有到达批量重偏向的时候,线程在同步对象的时候,做的是CAS操作,成功偏向锁,失败则撤销偏向,变为无锁,再升级为轻量级锁。

  4. 达到批量重偏向的时候,线程就要去判断,对象epoch的值和class的epoch是否相等,如果不相等,那么可以重偏向。
  5. 当达到批量撤销的时候,剥夺了该类的偏向锁功能,之后都会变为轻量级锁。


参考文档: https://www.cnblogs.com/LemonFive/p/11248248.html