偏向锁、轻量级锁、重量级锁

93 阅读6分钟

1、为啥要把这个放到一起比。

  • 因为这几个都是Synchronized内部的锁,由轻到重,逐渐膨胀。就像勇士斗魔王,一定最开始上来是杂兵、小怪、大怪最后才是魔王。
  • 加锁是因为多个线程竞争临界资源,只有一个线程竞争、两个线程去竞争、n多个线程竞争的激烈程度是不同的。竞争越激烈的情况下,获取锁的代价越大,所以为了减少性能消耗,jvm根据不同竞争情况,将锁分为偏向锁、轻量级锁、重量级锁

2、Mard Word

Synchronized锁的那个对象。java对象头的Mark Word中存储了HashCode、分代年龄、锁状态等信息, 屏幕截图 2023-12-03 133449.jpg

3、栈帧

方法执行时,在jvm的栈中会创建一个栈帧用来存储局部变量、操作数栈、动态链接、方法出口等信息。方法从调用到执行完成,就是栈帧在虚拟机栈中入栈到出栈的过程。(所以代码块中的局部变量可以实现入栈创建,出栈销毁)

线程中的许多方法同时处于执行状态,对执行引擎来说,活动线程中,栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧关联的方法称为当前方法。

4、偏向锁

  • HotSpot认为绝大部分情况,锁不存在竞争关系,也就是大部分情况就一个线程持有该同步代码块。为了减少互斥锁的代价。整了个偏向锁。
  • 偏向锁就是,把markWord中的线程ID标记为当前线程Id,就加锁陈工了。
  • 先读取Synchronized锁住的对象,检查锁偏向状态和锁标志位看是否可处于偏向状态。
    • 如果可偏向,就用CAS操作把线程id写到MarkWord当中,如果CAS操作成功则是获取锁成功,执行同步代码块。如果CAS操作失败,说明其他线程在竞争,并取到了偏向锁,那么等到全局安全点(GC运行之前所有线程都要在安全点则色,这个GC过程会触发STW),将偏向状态位设为0。验证已获取锁的线程是否存活,如果死了就把锁恢复到无锁状态,重新加锁。如果存活就把锁标记升级为轻量级锁。
    • 如果不可偏向的,先验证markword的中的线程id是不是当前线程,如果是就执行,如果不是,等待安全点,验证是否需要升级为轻量级锁。
  • 在无竞争切之后一个线程使用锁的情况下,我就用这玩意最最省性能。偏向锁只是在初始化的时候用一次CAS,轻量级锁,自旋用很多次CAS.每次申请、释放锁都至少需要一次CAS.
  • 偏向锁假定将来只有第一个申请锁的线程会使用锁,因此只需要再Mark word中用cas记录 当前线程的id。.如果记录成功则偏向锁获取成功,是否偏向锁位设置为1。以后再进来获取这个同步代码块的线程等于记录的线程id,就直接进,就相当于0成本拿锁。进来的不是这个线程,那么说明有其它线程会和你竞争。则膨胀为轻量级锁。
  • 偏向锁的锁状态在线程结束后,不会被设置为无锁状态,只有在新线程来获取锁的时候``,在安全点设置为无锁状态或者升级为轻量级锁

image.png

5、轻量级锁

  • 用了线程私有的LockRecord
  • 采用自旋CAS来操作,又称自旋锁。相比重量级锁,代价较小,如果自旋超过10次(默认10次,可以改)还没拿到,就升级为重量级锁。
  • 执行同步代码块之前,jvm在线程栈帧中创建一个存储锁记录的空间(Lock Record),并将Mark Word拷贝复制到锁记录中(因为已经脱离了原始的Mark Word,官方以displaced 作为前缀,即Displaced Mark Word(置换标记字)) ,然后尝试通过CAS将MarkWord中的锁记录的指针,指向到新创建的LockRecord。如果成功便是获取锁状态成功,如果失败就自旋。

image.png

  • CAS原理
    • 在主线程中有一个共享变量V,同时在各个子线程中有副本A。乐观锁就是,我先不管抢没抢到,算了再说,计算后的值为B。这个时候我才比较,A和V是否一样,如果一样就把B赋值给主线程的共享变量V。如果不一样,那么把V再赋值给A再计算一遍出B。再比对下V和A,如果不对再往复,如果对了,就B赋值给V。CAS机制中的这个步骤是原子性的(指令层面提供的原子操作),判断相等和赋值这两条指令原子性。所以CAS机制可以解决多线程并发编程对共享变量的原子性问题。当然实际不止比对值,还比对版本号。

6、重量级锁

  • 重量级锁(线程间共享的Object Monitor)的加锁需要通过MutexLock(互斥锁)和condition variable (个人理解condition variable提供了wait和notify来阻塞线程)来实现的。

  • 当轻量级锁膨胀到重量级锁之后,意味着线程只能被挂起阻塞来等待唤醒了。每一个对象中都有一个Monitor监视器,而Monitor依赖操作系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。

  • monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。而且当一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。我们可以简单的理解为,在加重量级锁的时候会执行monitorenter指令,解锁时会执行monitorexit指令。

  • 重量级锁会让抢占锁的线程从用户态变为内核态(如上下文切换和中断处理),开销大。

    • 上下文切换:当一个线程获得锁并进入临界区执行时,其他等待该锁的线程会被挂起并保存在内核空间中,直到该线程释放锁。在这个过程中,操作系统需要在内核空间进行上下文切换,保存和恢复被挂起线程的现场信息,这需要消耗一定的计算资源和时间。
    • 中断处理:当锁被释放时,操作系统需要在内核空间处理中断,通知等待该锁的线程,并恢复其现场信息,让其重新进入用户态执行。这个过程中也需要进行上下文切换和调度操作。

7、总结

锁膨胀这个过程,只允许升级,不允许降级,即只能偏向锁升级为轻量级锁,轻量级锁升级为重量级锁,不能反过来重量级锁降级为轻量级锁。