一、偏向撤销
在真正理解偏向撤销前需要区分清楚偏向撤销和偏向锁释放是两码事:
【撤销】:多线程竞争导致不能再使用偏向模式的时候,主要是告知这个锁对象不能再使用偏向模式。
【释放】:对应就是 synchronized 方法的推出或 synchroniezd 块的结束。
偏向撤销具体体现:
从偏向状态撤回原有的状态,也就是将 markword 的第 3 位(是否偏向撤销)的值,从 1 变回 0 (1代表偏向,0代表非偏向)。
@SneakyThrows
public void test8() {
TimeUnit.SECONDS.sleep(5); // 睡眠 5s
Object obj = new Object();
synchronized (obj) {
log.info("上半部分逻辑");
// 【2】当线程运行到此处,有其他线程竞争当前同步块,升级为轻量级锁
log.info("下半部分逻辑");
}
log.info("同步块结束之后的逻辑");
// 【1】当线程运行到此处,有其他线程竞争上述同步块,直接偏向撤销为无锁状态
}
【1】线程不存活或者活着的线程但退出了同步块,直接偏向撤销就好了;
【2】活着的线程但仍在同步块之内,那就要升级成轻量级锁;
4、之前的文章中提及偏向锁需要在特定的场景下才能提升程序的效率,可并不代表程序员写的程序都满足这些特定的场景,比如(开启偏向模式的前提下):
- 一个线程创建了⼤量对象并执⾏了初始的同步操作,之后在另⼀个线程中将这些对象作为锁,进⾏后续的操作。这种情况下,会导致⼤量的偏向撤销操作;
-
明知有多线程竞争,还要使⽤偏向锁,也会导致⼤量偏向撤销;
二、第一阶梯底线:批量重偏向
- 以class为单位,为每个类维护一个偏向锁撤销计数器,每一次该类的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时:BiasedLockingBulkRebiasThreshold = 20 ,JVM就认为该类的偏向锁有问题,因此就会进行批量重偏向,它的实现方式用到了epoch的概念。
- Epoch:含义是“纪元”,就是一个时间戳。每个class会有一个对应的epoch字段,每个处于 偏向锁状态对象的MarkWord 中也有该字段,其初始值为该类对应class中的eopch的值(此时二者是相等的)。每次发生批量重偏向,就将该值+1,同时遍历所有线程的栈:
- 【1】找到该class所有正处于加锁状态的偏向锁对象,将其epoch字段改为新值。
- 【2】class中不处于加锁状态偏向锁对象(没被任何线程持有,但之前被线程持有过的,这种锁对象的markword肯定也是有偏向的),保持epoch字段不变。
- 这样下次获得锁时,发现当前对象的epoch值和class的epoch不同,本着今朝不问前朝事的原则(上一个纪元),那就算当前线程已经偏向其他线程,也不会执行撤销操作,而是直接通过CAS操作将其markword的线程 ID 改为当前 ID ,这也算是一定程度的优化,毕竟没升级锁;如果epoch都一样,并且markword中有线程 ID ,还有其它锁来竞争,那锁自然是要升级的。
总结:在class的维度维护一个偏向锁撤销计数器,该类的对象只要发生偏向撤销操作就会给计数器+1,当计数器达到默认值20,JVM就会开始批量重偏向,将class中的epoch值+1,它会遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁对象,将其epoch值改为当前class中epoch的值,而不处于加锁状态的偏向锁对象(没被任何线程持有,但之前是被线程持有过的,这 种锁对象的 MarkWord 肯定也是有偏向的)则保持epoch值不变,这样在下次获得锁对象时,如果发现epoch值与当前class中的epoch值不一致,那就会通过CAS操作将其markword中的线程ID改成当前线程ID,依旧是已偏向状态,并未造成锁升级。
三、第二阶梯:批量撤销
- 当达到批量重偏向阈值之后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40)时, BiasedLockingBulkRevokeThreshold = 40 ,JVM就认为该class的使用场景存在较多并发锁竞争,会标记该class为不可偏向。之后对于该class的锁,直接走轻量级锁的逻辑,这就是第二阶梯底线,但是在第一阶梯到第二阶梯的过渡过程中,也就是在彻底禁用偏向锁之前,还给一次改过自新的机会,那就是另外一个计时器:BiasedLockingDecayTime = 25000
- 【1】如果距离上次批量重偏向发生的25秒之内,并且累计撤销计数达到40,就会发生批量撤销(偏向锁彻底over)
- 【2】如果距离上次批量重偏向发生的25秒之外,那么会重置在[20,40)内的计数,再给次机会
总结:在class中计数器的值越过第一阶梯(计数器的值大于20),后续分为两种情况
- 并且距离上一次发生第一阶段的防御(批量重偏向)25秒内,计数器的值迅速增大到40,JVM就会强制批量撤销,该class的对应的实例对象永远都不能再用偏向锁。
- 距离上一次发生第一阶段的防御(批量重偏向)时间超过25秒,那么会重置[20,40)内的计数
四、HashCode存储问题
- hashcode不是创建对象就帮我们写到对象头中的,⽽是要经过第⼀次调⽤Object#hashCode() 或者 调⽤ System#identityHashCode(Object) 才会存储在对象头中的。当第一次生成hashcode值之后,改值就会一直保持不变,但偏向锁中却没有存储hashcode的字段,那怎么办呢?
【结论】:即便初始化为可偏向的对象,一旦调用Object#hashCode() 或者 System#identityHashCode(Object),进入同步块就会使用轻量级锁,轻量级锁指针所指向的对象中可以存储。