synchronsized(3)-偏向撤销导致的问题及对应解决方案

一、偏向撤销

在真正理解偏向撤销前需要区分清楚偏向撤销和偏向锁释放是两码事:
【撤销】:多线程竞争导致不能再使用偏向模式的时候,主要是告知这个锁对象不能再使用偏向模式。
【释放】:对应就是 synchronized 方法的推出或 synchroniezd 块的结束。

偏向撤销具体体现

从偏向状态撤回原有的状态,也就是将 markword 的第 3 位(是否偏向撤销)的值,从 1 变回 0 (1代表偏向,0代表非偏向)。

1、如果只是⼀个线程获取锁,再加上“ 偏⼼ ”的机制,是没有理由撤销偏向的,所以 偏向撤销只能发⽣在有竞争的情况下
2、想要撤销偏向锁,还不能对持有偏向锁的线程有影响,所以就要等待持有偏向锁的线程到达⼀个 safepoint安全点 (这⾥的安全点是JVM 为了保证在垃圾回收的过程中引⽤关系不会发⽣变化设置的⼀种安全状态,在这个状态上会暂停所有线程⼯作 ),在这个安全点会挂起获得偏向锁的线程。
3、在这个安全点,线程可能还是处在不同状态的:
    @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、之前的文章中提及偏向锁需要在特定的场景下才能提升程序的效率,可并不代表程序员写的程序都满足这些特定的场景,比如(开启偏向模式的前提下):

  • 一个线程创建了⼤量对象并执⾏了初始的同步操作,之后在另⼀个线程中将这些对象作为锁,进⾏后续的操作。这种情况下,会导致⼤量的偏向撤销操作;
  • 明知有多线程竞争,还要使⽤偏向锁,也会导致⼤量偏向撤销;
5、一个偏向撤销的成本无所谓,大量偏向撤销的成本不可忽视。那怎么办呢?既不想禁用偏向锁,还不想忍受 ⼤量撤销偏向增加的成本,那么就设计⼀个有阶梯的底线的⽅案。

二、第一阶梯底线:批量重偏向

  • 以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),后续分为两种情况

  1. 并且距离上一次发生第一阶段的防御(批量重偏向)25秒内,计数器的值迅速增大到40,JVM就会强制批量撤销,该class的对应的实例对象永远都不能再用偏向锁。
  2. 距离上一次发生第一阶段的防御(批量重偏向)时间超过25秒,那么会重置[20,40)内的计数

四、HashCode存储问题

  • hashcode不是创建对象就帮我们写到对象头中的,⽽是要经过第⼀次调⽤Object#hashCode() 或者 调⽤ System#identityHashCode(Object) 才会存储在对象头中的。当第一次生成hashcode值之后,改值就会一直保持不变,但偏向锁中却没有存储hashcode的字段,那怎么办呢?

【结论】:即便初始化为可偏向的对象,一旦调用Object#hashCode() 或者  System#identityHashCode(Object),进入同步块就会使用轻量级锁,轻量级锁指针所指向的对象中可以存储。

  • 16
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值