JUC源码解析:深入解读偏向锁

JUC源码解析:深入解读偏向锁

本文使用 jdk8

几种锁状态介绍

先介绍一下锁状态吧

img

看偏向锁这一栏, 它的内存存储了 线程ID 和 Epoch , 这一点尤为关键, 意味着偏向锁没有内存可以存储对象头的 hashCode, 而其他锁是有地方存的.。也就意味着,,当锁对象被隐式(父类)或显试调用了 hashcode 时, 就不能进入偏向锁! , 下一节中我会着重介绍

深入偏向锁

  • 偏向锁默认在 jdk 启动几秒后才激活,可以使用JVM参数取消启动延迟:
    • -XX:BiasedLockingStartupDelay=0 为了便于实验,我之后的所有代码都会使用此参数

-XX:-UseBiasedLocking=false 可取消偏向锁

我先来一段代码,用实例看看Mark Word

static Object yyLock = new Object();

public static void main(String[] args) {


    System.out.println("=========== 进入 sync 代码块之前 =========");
    System.out.println(ClassLayout.parseInstance(yyLock).toPrintable());

    synchronized (yyLock) {
        System.out.println("=========== 进入 sync 代码块之后 =========");
        System.out.println(ClassLayout.parseInstance(yyLock).toPrintable());
    }

}

看看结果: 这两段输出分别是进入 sync 前和进入 sync 后的,因为输出太长我做了截选

新对象的mark Word默认就是偏向锁状态, 不过并没有存储偏向线程的id,而只是把值设置为了101

注意观察黄色框框

在进入同步块之前,偏向锁对象没有存储线程id,所以内存是空, 进入同步块之后,偏向锁对象存储了当前的线程ID

线程的id会保存在两个地方:

  • 锁的对象头:存储偏向线程id
  • 线程自己的栈帧,LOCK RECORD:存储自己的id

线程进入同步块时(重点!):会先把锁对象当成偏向锁,检测这两个地方的线程id

  • 如果测试成功,线程会直接获得锁。

  • 如果测试失败,会检查该锁在Mark Word 里是否为101偏向锁:

    • 如果不是偏向锁,CAS竞争锁

    • 如果是偏向锁,则尝试CAS将当前线程作为偏向线程!


值得一提的是,Mark Word中可以看到,偏向锁没有内存空间来保存对象的hashcode

偏向锁中能存储线程id和Epoch,但没有存储hashcode的位置了

所以说,如果一个对象显式或隐式地调用方法生成了 hashcode(),那么,那么偏向锁只能被迫升级为轻量级锁

举一个代码示例:

static Object yyLock = new Object();

public static void main(String[] args) {


    System.out.println("=========== 进入 sync 代码块之前 =========");
    System.out.println(ClassLayout.parseInstance(yyLock).toPrintable());

    // 隐式生成了hashcode
    HashMap map = new HashMap<>();
    map.put(yyLock, "");

    synchronized (yyLock) {
        System.out.println("=========== 进入 sync 代码块之后 =========");
        System.out.println(ClassLayout.parseInstance(yyLock).toPrintable());
    }

}

这时候看看输出:

锁升级了,偏向锁成为了轻量级锁

偏向锁的锁升级

偏向锁使用了一直到竞争出现才会释放锁的机制,一旦出现竞争环境,偏向锁就会升级为轻量级锁

但是

一定会升级吗?一定是竞争才会升级吗?一定会升级为轻量级锁吗?

一、不出现竞争也可能升级

​ 若有锁对象lock、线程A、线程B。线程B要在线程A死亡后执行。

​ 线程A获取lock轻量级锁,lock对象头存储A的线程ID,线程A释放轻量级锁,线程A死亡。lock仍存储着A的线程ID

​ 线程B在线程A死亡后拿到lock偏向锁,lock对象头仍存储着A的线程ID,这时,没有发生锁争抢,但偏向锁lock升级为轻量级锁。

​ 线程B释放lock锁后,lock由轻量级锁释放为无锁状态。

二、偏向锁直接升级为重量级锁

​ 若有锁对象lock,线程A、线程B。

​ 线程A获取lock偏向锁锁,线程A还没有释放lock偏向锁锁,这时线程B来争抢lock,lock会直接升级成重量级锁。

三、出现竞争时升级为轻量级锁

​ 线程A获取偏向锁,线程A释放了偏向锁,但线程A还没有死亡,这时,线程B来争抢偏向锁,部分偏向锁会升级为轻量级锁、部分偏向锁仍保持偏向锁

只理解为升级为轻量级锁就行了。保持偏向锁的部分建议忽视不做深究。

四、升级为重量级锁后又变成轻量级锁(不建议深究了)

​ 若有锁对象lock,线程A、线程B。

​ 线程A获取lock轻量级锁,线程A还没有释放lock,线程B去争抢lock,这时,lock轻量级锁升级为重量级锁

​ 待线程A、线程B均释放lock且死亡,lock重量级锁变成lock轻量级锁。

偏向锁的撤销

​ 若有偏向锁 lock、线程A、线程B

​ 线程A访问同步块,检查lock对象头中是否存储了A的线程ID,如果有,就获取偏向锁,如果没有,通过CAS自旋将自己的线程ID写入lock MarkWord中。

​ 此时,线程A已经获取到了lock偏向锁,线程B要争抢偏向锁,首先,线程B会检查lock对象头是否存储了自己的线程ID,发现没有存储,尝试CAS替换lock MarkWord,失败了,会发起撤销偏向锁请求。

​ B发起的撤销请求会使线程A暂停,让线程A进入安全点,这时线程A会解锁,线程A移除掉自己栈帧里的 LOCK RECORD(存储线程id),同时删除MarkWord里的线程ID,最后恢复线程。

​ 此时线程B就有机会争抢锁了,会将其争抢为轻量级锁。

批量重偏向 和 批量锁撤销

​ 若有一批以相同Class new 出来的对象作为锁,让线程A、B、C 争抢这一批对象锁

​ A线程获取这一批偏向锁,之后A释放这一批偏向锁,并且A不再是活跃线程。

​ A不再活跃后,有B线程来获取这一批偏向锁,这些偏向锁会升级成轻量级锁。

批量重偏向:

​ 如果B一直获取,达到大约20次阈值,此时会触发批量重偏向,jvm会让之后的锁对象直接偏向线程B,B之后获取到的回是偏向锁。

批量撤销:

​ 在基于批量重偏向的基础上,还在继续进行争抢这批锁达到约40次阈值,并且有第三条线程C加入,这时会触发批量撤销。JVM会标记该Class的对象不能使用偏向锁,以后新创建的对象直接以轻量级锁开始。这是真正完成了锁升级。

对于偏向锁来说,真正的锁升级是依赖于 Class 的,而不是依赖于某一个对象的。 发生批量撤销后,使用这个Class new出来的对象,都不能使用偏向锁,而是直接以轻量级锁开始的。

posted @ 2024-05-07 13:55  yangruomao  阅读(20)  评论(0编辑  收藏  举报