以下截图及相关信息,均来源于马士兵公开课中
锁升级的过程
锁升级过程图:
锁升级过程:
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
升级策略:
有线程超过10次自旋, -XX:PreBlockSpin, 或者自旋线程数超过CPU核数的一半;
在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
- 重量级锁,需要向操作系统申请,操作系统的锁个数是一定的。
-
偏向锁
更偏向于第一个调用它的线程。
默认开启,可以关闭。