JVM 内置锁 synchronized 关键字,偏向锁优化中的批量重偏向和批量撤销

884 阅读4分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。

批量重偏向和批量撤销

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

以 class 为单位,每个class 维护一个偏向锁撤销计数器,每一次该 class 的对象发生偏向撤销操作时,该计数器 +1 ,当这个值达到重偏向阈值(默认 20 )时,JVM 就认为该 class 的偏向锁有问题,因此会进行批量重偏向。

每个 class 对象会对应一个 epoch 字段,每个处于偏向锁状态的对象的 mark word 也有该字段,其初始值为创建该对象的 class 的 epoch 的值,每次发生批量重偏向时,就将该值 +1 ,同时遍历 JVM 中所有线程的栈,找到该 class 所有正处于加锁状态的偏向锁,将其 epoch 字段更新为新值,下次获得锁时,发现该对象的 epoch 值和 class 的 epoch 值不想等,那就算当前已经偏向了其他的线程,也不会执行撤销操作,而是直接通过 CAS将其 Mark word 的 thread id 修改为当前线程 id

当达到重偏向阈值(默认 20)后, 假设该 class 计数器继续增长,当其达到批量撤销的阈值(默认40) JVM 就认为该 class 的使用场景存在多线程竞争,会标记当前 class 不可偏向,之后,对于该 class 的锁,直接走轻量级锁的逻辑。

运用场景(解决的问题):

1、批量重偏向(bulk rebias) 机制:一个线程创建了大量对象并且执行了初始的同步操作,后来另外一个线程也来将这些对象作为锁进行操作,这样会导致大量的偏向锁撤销操作。

2、批量撤销(bulk revoke) 机制:在明显多线程竞争剧烈的场景下使用偏向锁时不合适的。

Java 系统默认参数查询

java -XX:+PrintFlagsFinal

我们可以通过-XX:BiasedLockingBulkRebiasThreshold-XX:BiasedLockingBulkRevokeThreshold 来手动设置阈值。默认值如下: image.png

批量偏向锁重偏向

实验代码如下:

public class A1 {
    public static void main(String[] args) throws Exception {
        //延时产生可偏向对象
        Thread.sleep(5000);

        //创造100个偏向线程t1的偏向锁
        List<A> listA = new ArrayList<>();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                A a = new A();
                synchronized (a) {
                    listA.add(a);
                }
            }
            try {
                //为了防止JVM线程复用,在创建完对象后,保持线程t1状态为存活
                Thread.sleep(100000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();

        //睡眠3s钟保证线程t1创建对象完成
        Thread.sleep(3000);
        System.out.println("打印t1线程,list中第20个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(listA.get(19)).toPrintable()));

        //创建线程t2竞争线程t1中已经退出同步块的锁
        Thread t2 = new Thread(() -> {
            //这里面只循环了30次!!!
            for (int i = 0; i < 30; i++) {
                A a = listA.get(i);
                synchronized (a) {
                    //分别打印第19次和第20次偏向锁重偏向结果
                    if (i == 18 || i == 19) {
                        System.out.println("第" + (i + 1) + "次偏向结果");
                        System.out.println((ClassLayout.parseInstance(a).toPrintable()));
                    }
                }
            }
            try {
                Thread.sleep(10000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t2.start();

        Thread.sleep(3000);
        System.out.println("打印list中第11个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(listA.get(10)).toPrintable()));
        System.out.println("打印list中第26个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(listA.get(25)).toPrintable()));
        System.out.println("打印list中第41个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(listA.get(40)).toPrintable()));
    }
}

我们一起来看看结果,在 thread0 中创建的第 20 个共享锁对象,当前是偏向锁,线程 id -594241531

image.png

再来看看 t2 线程,前19次均产生了轻量级锁,到了20次时候到达批量重偏向的阈值20 ,此时不是轻量级锁,而变成了偏向锁,此时偏向锁线程 t2 , 线程 id : -619083515

image.png

再看看偏向锁结束后的对象头信息。 前 20 个对象并没有触发批量重偏向机制,线程 t2 释放锁之后,变成无锁状态;

第 20 - 30 个对象,触发了批量重偏向机制,对象为偏向锁状态,偏向线程 t2, 线程 t2 的 id 为-619083515

第 31 个对象之后,也没有触发了批量重偏向机制,对象依然偏向线程 t1 , 线程 t1 的 id 是 -594241531 image.png 注意:这里建议共享对象为自定义类对象

批量偏向锁撤销

当撤销偏向锁的阈值超过 40 次过后, jvm 会认为不该偏向,于是整个类的所有对象都会变成不可偏向,新创建的对象也不可偏向。 注意: 对于统计批量撤销有个参数:-XX:BiasedLockingDecayTime=25000ms范围内没有达到 40 次,撤销次数清 0 ,重新记时

public class B1 {

    public static void main(String[] args) throws Exception {

        Thread.sleep(5000);
        List<A> listA = new ArrayList<>();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i <100 ; i++) {
                A a = new A();
                synchronized (a){
                    listA.add(a);
                }
            }
            try {
                Thread.sleep(100000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();
        Thread.sleep(3000);

        Thread t2 = new Thread(() -> {
            //这里循环了40次。达到了批量撤销的阈值
            for (int i = 0; i < 40; i++) {
                A a =listA.get(i);
                synchronized (a){
                }
            }
            try {
                Thread.sleep(10000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t2.start();

        Thread.sleep(3000);
        System.out.println("打印list中第11个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(listA.get(10)).toPrintable()));
        System.out.println("打印list中第26个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(listA.get(25)).toPrintable()));
        System.out.println("打印list中第90个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(listA.get(89)).toPrintable()));


        Thread t3 = new Thread(() -> {
            for (int i = 20; i < 40; i++) {
                A a =listA.get(i);
                synchronized (a){
                    if(i==20||i==22){
                        System.out.println("thread3 第"+ i + "次");
                        System.out.println((ClassLayout.parseInstance(a).toPrintable()));
                    }
                }
            }
        });
        t3.start();


        Thread.sleep(10000);
        System.out.println("重新输出新实例A");
        System.out.println((ClassLayout.parseInstance(new A()).toPrintable()));
    }
}

看看输出结果,这部分和上面的批量偏向有些不同。重点关注线程 id

前 20 个对象并没有触发批量重偏向机制,线程 t2 释放锁之后,变成无锁状态;

第 20 - 40 个对象,触发了批量重偏向机制,对象为偏向锁状态,偏向线程 t2, 线程 t2 的 id 为 -1005618939

第 41 个对象之后,也没有触发了批量重偏向机制,对象依然偏向线程 t1 , 线程 t1 的 id 是 -1039208443

image.png

t3 线程开始进行锁的竞争,因为已经到到批量撤销的阈值,且对象 listA.get(20) 和 listA.get(22) 已经进行了锁的重偏向了,所以 t3 再次获取锁的时候,不会触发重新偏向为线程 t3.

此刻,触发批量撤销,此对象碰撞变为轻量级锁

image.png

最后我们看一下新生成的对象。本来应该是可偏向状态,但是在经历过批量重偏向,和批量撤销之后转换为无锁。

image.png

批量重偏向和批量撤销总结

  • 批量重偏向和批量撤销针对类的优化,和对象无关。
  • 批量锁重新偏向一次过后不能重新偏向。
  • 当某个类已经触发批量撤销机制后, JVM 会默认当前类长生了严重的问题,剥夺了该类新生成对象偏向锁的权利。

参考资料