concurrency编程—偏向锁浅析

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

jvm延迟偏向锁

无锁->轻量级锁(没有资源竞争)->重量级锁(有资源竞争)
重量级锁释放之后会变成无锁,每次加锁释放锁要操作os

• 如果调用wait()方法,会立即变成重量级锁

来自 https://blog.csdn.net/qq_32223565/article/details/107068482

同步:
1.单个线程--------------------------------偏向锁
2.多个线程交替执行-------------------轻量锁(自旋)
3.多个线程互斥执行-------------------重量锁

偏向锁:
输出结果

刚开始使用这段代码我是震惊的,为什么睡眠了5s中就把活生生的A对象由无锁状态改变成为偏向锁了呢?别急,容我慢慢道来!

JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁。这个延时的时间大概为4s左右,具体时间因机器而异。当然我们也可以设置JVM参数 -XX:BiasedLockingStartupDelay=0 来取消延时加载偏向锁。

可能你又要问了,我这也没使用synchronized关键字呀,那不也应该是无锁么?怎么会是偏向锁呢?
仔细看一下偏向锁的组成,对照输出结果红色划线位置,你会发现占用 thread 和 epoch 的 位置的均为0,说明当前偏向锁并没有偏向任何线程。此时这个偏向锁正处于可偏向状态,准备好进行偏向了!你也可以理解为此时的偏向锁是一个特殊状态的无锁。

来自 https://www.cnblogs.com/LemonFive/p/11246086.html

加锁后,对象头生成线程id,释放锁后,对象头信息不变
jvm进入同步块,要看这个对象是否可偏向
偏向锁无法重新偏向,只偏向第一个
批量重偏向:当一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,会导偏向锁重偏向的操作。
批量撤销:在多线程竞争剧烈的情况下,使用偏向锁将会降低效率,于是乎产生了批量撤销机制。

来自 https://www.cnblogs.com/LemonFive/p/11248248.html

intx BiasedLockingBulkRebiasThreshold = 20 默认偏向锁批量重偏向阈值
intx BiasedLockingBulkRevokeThreshold = 40 默认偏向锁批量撤销阈值
当然我们可以通过-XX:BiasedLockingBulkRebiasThreshold 和 -XX:BiasedLockingBulkRevokeThreshold 来手动设置阈值

来自 https://www.cnblogs.com/LemonFive/p/11248248.html

1、批量重偏向和批量撤销是针对类的优化,和对象无关。
2、偏向锁重偏向一次之后不可再次重偏向。
3、当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利

来自 https://www.cnblogs.com/LemonFive/p/11248248.html

偏向锁逻辑1.线程A第一次访问同步块时,先检测对象头Mark Word中的标志位是否为01,依此判断此时对象锁是否处于无所状态或者偏向锁状态(匿名偏向锁);
2.然后判断偏向锁标志位是否为1,如果不是,则进入轻量级锁逻辑(使用CAS竞争锁),如果是,则进入下一步流程;
3.判断是偏向锁时,检查对象头Mark Word中记录的Thread Id是否是当前线程ID,如果是,则表明当前线程已经获得对象锁,以后该线程进入同步块时,不需要CAS进行加锁,只会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,用来统计重入的次数(如图为当对象所处于偏向锁时,当前线程重入3次,线程栈帧中Lock Record记录)。

退出同步块释放偏向锁时,则依次删除对应Lock Record,但是不会修改对象头中的Thread Id;
注:偏向锁撤销是指在获取偏向锁的过程中因不满足条件导致要将锁对象改为非偏向锁状态,而偏向锁释放是指退出同步块时的过程。
4.如果对象头Mark Word中Thread Id不是当前线程ID,则进行CAS操作,企图将当前线程ID替换进Mark Word。如果当前对象锁状态处于匿名偏向锁状态(可偏向未锁定),则会替换成功(将Mark Word中的Thread id由匿名0改成当前线程ID,在当前线程栈中找到内存地址最高的可用Lock Record,将线程ID存入),获取到锁,执行同步代码块;
5.如果对象锁已经被其他线程占用,则会替换失败,开始进行偏向锁撤销,这也是偏向锁的特点,一旦出现线程竞争,就会撤销偏向锁;
6.偏向锁的撤销需要等待全局安全点(safe point,代表了一个状态,在该状态下所有线程都是暂停的),暂停持有偏向锁的线程,检查持有偏向锁的线程状态(遍历当前JVM的所有线程,如果能找到,则说明偏向的线程还存活),如果线程还存活,则检查线程是否在执行同步代码块中的代码,如果是,则升级为轻量级锁,进行CAS竞争锁;
注:每次进入同步块(即执行monitorenter)的时候都会以从高往低的顺序在栈中找到第一个可用的Lock Record,并设置偏向线程ID;每次解锁(即执行monitorexit)的时候都会从最低的一个Lock Record移除。所以如果能找到对应的Lock Record说明偏向的线程还在执行同步代码块中的代码。
7.如果持有偏向锁的线程未存活,或者持有偏向锁的线程未在执行同步代码块中的代码,则进行校验是否允许重偏向,如果不允许重偏向,则撤销偏向锁,将Mark Word设置为无锁状态(未锁定不可偏向状态),然后升级为轻量级锁,进行CAS竞争锁;
8.如果允许重偏向,设置为匿名偏向锁状态,CAS将偏向锁重新指向线程A(在对象头和线程栈帧的锁记录中存储当前线程ID);
9.唤醒暂停的线程,从安全点继续执行代码。
以上便是偏向锁的整个逻辑了。
延申知识
批量重偏向与批量撤销渊源:从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制。
解决场景批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。
原理以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该字段,其初始值为创建该对象时class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id。当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

来自 https://baijiahao.baidu.com/s?id=1630535202760061296&wfr=spider&for=pc

t2 synchronized流程
我们来仔细分析一下t2中第1-39(下标0-38)个Dog锁对象的synchronized过程,就能理解了。

  1. 第1-19:只说是偏向锁的情况,先进行一系列判断(是不是偏向当前线程,偏向模式关闭的话撤销偏向锁,能不能重偏向),发现上述判断都不成立(偏向t1,偏向模式没有关闭,不能重偏向),进入锁升级。在锁升级这一步里,会把撤销次数+1,执行单次撤销锁操作。
  2. 第20:跟1中一样进入锁升级,把撤销次数+1,此时撤销次数==20,就执行批量重定向操作。具体如下:1.类Dog的epoch+1(00-01);2.把类Dog的所有处在加锁状态的锁对象的epoch更新为类Dog的epoch;3. 执行重偏向操作。
  3. 第21-39:先执行1中的一系列判断,此时发现可以进行重偏向,(重偏向的条件就是锁对象的epoch!=类的epoch),那就直接重偏向了,重偏向时会把锁对象epoch更新为类的epoch。不会进入锁升级,也不会+撤销次数了!
    t3 synchronized流程
    前19个就不用说了,在t2中都变成轻量级锁了。关键看第20-39,此时它们都是偏向锁,那也会执行分析t2时提到的一系列判断,但这时就不满足重偏向条件了,因为在t2的重偏向执行完后,20-38锁对象的epoch和类的epoch就一样了。那就会进入锁升级的流程,也就是20次撤销操作。
    到此为止就能解释所有的问题了。最后简单说一下epoch。
    epoch
    epoch是Mark Word中的一部分,只存在于偏向锁状态,占2位。锁对应的类同样用2位存了epoch。
    epoch的作用就是标记偏向的合法性,说通俗点,就是看这个偏向锁有没有其他线程正在用。如果不用epoch,那每次想要重偏向,都得去遍历所有的线程栈看看有没有其他线程在用。
    再完整解释一遍:每次撤销数量刚到20的时候,锁的类epoch都会+1,并且更新加锁状态的同类锁对象,那么那些不加锁的锁对象epoch就和类的epoch不一样了,那就可以知道哪些偏向锁是空闲的了。
    epoch只有两位,那肯定会循环,但不会影响准确性。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值