JVM笔记 - Java 虚拟机关于 Synchronized 实现以及锁实现的总结

本文是我在阅读 《深入理解Java虚拟机-第三版》和 极客时间 郑宇迪对于JVM的剖析后做的总结,如有不妥,不明白的地方,欢迎斧正
下面是一张比较经典的 Java 虚拟机锁实现流程图,不了解JVM的具体锁实现可能会不太明白
在这里插入图片描述

字节码上的实现

声明 synchronized 代码块时,编译而成的字节码将包含 monitorentermonitorexit 指令,一般会包含一个monitorenter和多个monitorexit,确保在函数所有的出口都能释放锁

sycnchronized 声明函数时,会在访问标志中声明 ACC_SYNCHRONIZED ,是告诉 JVM 需要对该函数进行加锁解锁,声明函数时,在 Code 中,monitorenter和monitorexit都是隐式的

通过monitorenter和monitorexit对锁对象的锁技术器进行加减,为0时,意味着没有被任何对象持有,并且实现了锁的可重入的特性

JVM 中锁具体实现

内存布局中对象头的标记字段(Mark Word)最后两位,用来表示所属对象的锁状态

00代表轻量级锁,01代表无锁或者偏向锁,10代表重量级锁,11 则是 GC算法标志

在这里插入图片描述

重量级锁

  1. 传统锁是JVM最基础的锁实现,因为性能开销大,也叫重量级锁
  2. 如何实现: 通过系统内核互斥实现线程阻塞和唤醒
  3. 性能开销大(涉及系统调用,从系统用户态切换到内核态)

锁优化 - 自旋

  1. 自旋锁通过线程轮询锁是否被释放,那么此线程就不会被挂起,而是一直占用着cpu资源
  2. 部分线程的逻辑消耗小于性能调用的消耗时,自旋锁可以避免系统调用的性能消耗(优势)
  3. 占用CPU资源,跑无用指令(期望占用cpu资源时,目标锁资源能够尽快释放)
  4. 默认的自旋次数是10次,超过了限定次数,将会调用内核的方式挂起该线程
  5. 带来了不公平的锁机制,其他阻塞线程,没办法和自旋状态的线程竞争锁资源

自适应自旋

根据以往自旋等待时是否能够获得锁,来动态调整自旋的时间(循环数目)实现自适应自旋
,避免自旋锁可能出现大量浪费处理器资源(轮询锁资源过久,锁仍未释放),自适应意味着自旋时间或次数不再固定

实现原理: 动态调整由前一次在同一个锁上的自旋时间以及持有目标锁资源的线程的状态决定

轻量级锁

'轻量级’只是比较操作系统的传统锁而言,理想应用场景在没有多线程竞争的前提下,减少传统锁的性能开销

实现原理:JVM 通过 CAS 实现轻量级锁

加锁

  1. 进行加锁时,判断锁资源是否为重量级锁
  2. 如果不是,那么会在当前线程(执行到同步代码的线程)中划出一块空间(Lock Record),作为锁的记录,将目标锁对象的标记字段复制到锁记录中
  3. JVM 尝试用 CAS 操作替换目标锁对象的标记字段

1.将目标锁对象的 Mark Word 更新为 Lock Record 的指针
2.更新成功,当前线程持有了该锁对象,失败则代表已被抢先一步抢走了锁,锁对象的Mark Word指针地址被修改了
3.如果被其他线程占有了该锁资源,那么轻量级锁不再有效,膨胀为重量级锁(00–>10),JVM会将锁对象的锁标志变为 ‘10’,等待线程必须进入阻塞状态

解锁

  1. 解锁也是通过 CAS 操作实现
  2. 若锁对象的 Mark Word 仍然指向线程 t 栈帧中的 Lock Record
  3. 借助 CAS 操作将锁对象的 Mark Word 替换为栈帧中复制的 Lock Record:
  4. 替换成功,则解锁成功,线程 t 离开同步块;
  5. 替换失败,说明有其他线程竞争过该锁,锁已经膨胀为重量级锁,唤醒其他阻塞锁

并不是说轻量级锁性能一定比重量级锁好
1. 理想条件:最优情况是大部分锁,是不存在竞争情况的
2. 如果存在有竞争的情况,那么不仅仅是调用系统内核的mutex还要多增CAS操作的损耗

偏向锁

引用场景: 从始至终只有一个线程请求某一把锁,优化掉同步操作消耗的性能

线程进行加锁,如果该锁对象支持偏向锁,那么 JVM 会通过 CAS 操作,将当前线程的地址记录在锁对象的标记字段中,并且将标记字段的最后三位设置为 101(偏向模式:1,最后两位锁状态:01)

之后的运行过程中,每当有线程请求这把锁,Java 虚拟机只需判断锁对象标记字段中:

  1. 最后三位是否为 101
  2. 是否包含当前线程的地址,
  3. 以及 epoch 值是否和锁对象的类的 epoch 值相同

如果都满足,那么当前线程持有该偏向锁,可以直接返回

偏向锁的撤销

当请求锁资源的线程和锁对象标记保持的线程不匹配时,JVM需要撤销偏向锁

  1. 偏向锁的撤销动作必须等待全局安全点;
  2. 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态;
  3. 撤销偏向锁,恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态;

如果锁对象总撤销次数超过了阈值(XX:BiasedLockingBulkRebiasThreshold,默认为 20),那么 JVM 会宣布这个资源的偏向锁失效

如果总撤销数超过40次(默认),jvm认为该类不再适合偏向锁,之后的加锁都会转变后轻量级锁

epoch 是什么

某个类的偏向锁失效时,JVM 将该类 epoch 值+1,表示之前epoch值的偏向锁失效

而新设置的偏向锁需要复制新的epoch,为了保证持有锁资源的线程不会丢锁,JVM 需要遍历所有线程的栈,找到该类加锁的实例,并且将他们的 Mark Word中的epoch + 1,该操作需要所有线程处于安全点

参考资料

极客时间-深入理解JVM

synchronized 原理(三):偏向锁、轻量级锁

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读