Synchronized升级过程

以下截图及相关信息,均来源于马士兵公开课中


锁升级的过程

锁升级过程图:

image.png
image.png

锁升级过程:

new - 偏向锁 - 轻量级锁 (无锁, 自旋锁,自适应自旋)- 重量级锁

synchronized优化的过程和markword息息相关;

用markword中最低的三位代表锁状态 其中1位是偏向锁位 两位是普通锁位

一、无锁态:

通过 new 关键字创建对象

二、偏向锁:

偏向锁由于有锁撤销的过程revoke,会消耗系统资源,所以,在锁争用特别激烈的时候,用偏向锁未必效率高。还不如直接使用轻量级锁。

升级过程:

当线程调用这个对象时,发现这个对象没有被任何线程使用,会把指向当前的线程的指针(JavaThread*),放到对象头 markword 中,用于标记。锁升级为偏向锁。下次线程再次调用发现还是本线程的指针,无需再次上锁,直接调用。

markwork 存储值:

上偏向锁,指的就是,把markword的线程ID改为自己线程ID的过程;偏向锁不可重偏向 批量偏向 批量撤销

markword 上记录当前线程指针,下次同一个线程加锁的时候,不需要争用,只需要判断线程指针是否同一个,所以,偏向锁,偏向加锁的第一个线程 。hashCode备份在线程栈上 线程销毁,锁降级为无锁

三、轻量级锁:

升级原因:

当多个线程调用时,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。

升级过程:

如果有线程竞争,撤销偏向锁,升级轻量级锁。线程在自己的线程栈生成LockRecord ,用CAS去争用markword的LR的指针,指针指向哪个线程的LR,哪个线程就拥有锁

线程抢夺锁:

每个线程都有自己的线程栈,在自己的线程栈生成一个自己的对象【Lock Record】;看谁能把自己的 Lock Record 贴到对象上,谁就拥有这把锁。抢的过程,通过自旋(CAS) 的方式抢夺,读取对象中的 Lock Record,并且判断是否可以修改,在回写本线程的Lock Record 指针到对象时,判断是不是自己取的那个值。

markwork 存储值:

线程在自己的线程栈生成LockRecord ,用CAS操作将markword设置为指向自己这个线程的LR的指针。

四、重量级锁

升级原因:

当自旋线程过多,执行线程占用时间又长。自旋会消耗大量CPU资源。不如升级为重量级锁,进入等待队列(不消耗CPU)-XX:PreBlockSpin

升级策略:
  1. 有线程超过10次自旋, -XX:PreBlockSpin, 或者自旋线程数超过CPU核数的一半;

  2. 在1.6之后,加入自适应自旋 Adapative Self Spinning , JVM自己控制

升级过程:

升级重量级锁 -> 向操作系统申请资源,获得 Linux mutex【锁】。CPU从3级【用户态】-0级系统调用【内核态】,线程挂起,进入等待队列,等待操作系统的调度,然后映射到用户空间。

markwork 存储值:

指向互斥量(重量级锁mutex )的指针。


锁重入

sychronized是可重入锁

重入次数必须记录,因为要解锁几次必须得对应

偏向锁 自旋锁 -> 线程栈 -> LR + 1

重量级锁 -> ? ObjectMonitor字段上

问题:

为什么有自旋锁还需要重量级锁?

自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗

重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源

偏向锁是否一定比自旋锁效率高?

不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁

JVM启动过程,会有很多线程竞争(明确),所以默认情况启动时不打开偏向锁,过一段儿时间再打开

批量重偏向与批量撤销渊源:

从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制。

原理以class为单位,为每个class维护解决场景批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。

一个偏向锁撤销计数器,每一次该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的锁,直接走轻量级锁的逻辑。

小知识:

  • 整个程序的执行状态分为

    • 用户态,内核态。内核态是非常核心的跟内核、硬件打交道的操作只有它能执行。【比如往网卡、显卡上写数据。】
  • 分代年龄

    • 一个对象被垃圾回收器回收一次,年龄会+1 ,年龄到达一定程度,这个对象会从 “年轻代” 升级到 ”老年代“。分代年龄在 JVM 里面是可以通过参数控制的,分代年龄两种默认值,第一种模式 15 ,PS+PO回收器 ;第二种模式 6 ,使用 cms 回收器 。4 Bit最大值15。
  • synchronized

    • 重量级锁,需要向操作系统申请,操作系统的锁个数是一定的。
  • 偏向锁

    • 更偏向于第一个调用它的线程。

    • 默认开启,可以关闭。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容