欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

关于synchronized批量重偏向和批量撤销的一个小实验

程序员文章站 2022-07-10 17:43:51
前段时间学习synchronized的时候做过一个关于批量重偏向和批量撤销的小实验,感觉挺有意思的,所以想分享一下。虽然是比较底层的东西,但是结论可以通过做实验看出来,就挺有意思。我们都知道synchronized分为偏向锁、轻量级锁和重量级锁这三种,这个实验主要是和偏向锁相关的。关于偏向锁,我们又知道,偏向锁在偏向了某一个线程之后,不会主动释放锁,只有出现竞争了才会执行偏向锁撤销。先说结论吧,开启偏向锁时,在「规定的时间」内,如果偏向锁撤销的次数达到20次,就会执行批量重偏向,如果撤销.....

前段时间学习synchronized的时候做过一个关于批量重偏向和批量撤销的小实验,感觉挺有意思的,所以想分享一下。虽然是比较底层的东西,但是结论可以通过做实验看出来,就挺有意思。

我们都知道synchronized分为偏向锁、轻量级锁和重量级锁这三种,这个实验主要是和偏向锁相关的。关于偏向锁,我们又知道,偏向锁在偏向了某一个线程之后,不会主动释放锁,只有出现竞争了才会执行偏向锁撤销。

先说结论吧,开启偏向锁时,在「规定的时间」内,如果偏向锁撤销的次数达到20次,就会执行批量重偏向,如果撤销次数达到了40次,就会触发批量撤销。批量重偏向和批量撤销都可以理解为虚拟机的一种优化机制,我理解主要是出于性能上的考虑。

当一个锁对象类的撤销次数达到20次时,虚拟机会认为这个锁不适合再偏向于原线程,于是会在偏向锁撤销达到20次时让这一类锁尝试偏向于其他线程。

当一个锁对象类的撤销次数达到40次时,虚拟机会认为这个锁根本就不适合作为偏向锁使用,因此会将类的偏向标记关闭,之后现存对象加锁时会升级为轻量级锁,锁定中的偏向锁对象会被撤销,新创建的对象默认为无锁状态。


下面先说明一下实验之前要准备的东西。首先,我们得开启偏向锁,关闭偏向锁的延迟启动。由于只有看到对象头的锁标志位才能判断锁的类型,因此还需要用到OpenJDK提供的JOL(Java Object Layout)包。由于下面讲解涉及到对象头的mark word,这里贴一张mark word的说明图。epoch可以理解为锁对象的年龄标记,利用JOL查看对象头时主要关注锁标志位即可。

锁对象mark word的低三位为001时,表示无锁且不可偏向,若为101则表示匿名偏向或偏向锁定状态(取决于是否有Thread ID)。锁对象类也有锁标志位的概念,作用和锁对象类似,我理解只是作用范围的区别。锁对象类若为不可偏向,所有新创建的对象都是不可偏向的。

关于synchronized批量重偏向和批量撤销的一个小实验

实验代码包括39个锁对象和3个线程:T1、T2、T3,分三次执行加锁解锁操作,因此锁对象的状态的变化也分为三个阶段。

第一阶段是T1线程执行。T1线程执行后,由于是第一次加锁,因此所有对象都偏向于T1。

此时从对象头mark word可以看出,所有对象都处于偏向锁定状态(偏向于T1)。

05 70 51 19 (00000101 01110000 01010001 00011001) (424767493)

00 00 00 00 (00000000 00000000 00000000 00000000) (0)

第二阶段是T2线程执行。T2线程执行后,0~18号对象会执行偏向锁撤销,锁对象状态变化为:偏向锁->轻量级锁->无锁。偏向锁撤销执行到19号对象,也就是第20个锁对象时,会触发批量重偏向,此时19~38号对象会批量重偏向于T2。实际上此时只会修改类对象的epoch和处于加锁中的锁对象的epoch(也就是说不会重偏向处于使用中的锁对象),其他未处于加锁中的锁对象的重偏向则发生于下一次加锁时,判断条件是类对象epoch和锁对象epoch是否一致,不一致则会执行重偏向。T2退出同步代码后的最终结果就是0~18号对象变为无锁状态,19~38号对象偏向于T2,偏向锁撤销次数为20次。

此时从对象头mark word可以看出,0~18号对象处于无锁状态,19~38号对象则处于偏向锁定状态(偏向于T2)。

0~18号对象:

01 00 00 00 (00000001 00000000 00000000 00000000) (1)

00 00 00 00 (00000000 00000000 00000000 00000000) (0)

19~38号对象:

05 99 51 19 (00000101 10011001 01010001 00011001) (424777989)

00 00 00 00 (00000000 00000000 00000000 00000000) (0) 

第三阶段是T3线程执行。此时0~18已经处于无锁状态,只能加轻量级锁。19~38号对象则有所不同,这20个对象执行时会逐个执行偏向锁撤销,到第38号对象时刚好又执行了20次,此时总的撤销次数到达40次,于是触发批量撤销。批量撤销会将类的偏向标记关闭,之后现存对象加锁时会升级为轻量级锁,锁定中的偏向锁对象会被撤销,新创建的对象默认为无锁状态。

此时从对象头mark word可以看出,0~37号对象处于无锁状态,38号对象也处于无锁状态(升级成轻量级锁后又解锁了)。

01 00 00 00 (00000001 00000000 00000000 00000000) (1)

00 00 00 00 (00000000 00000000 00000000 00000000) (0)


以上步骤是常规步骤,如果把「sleep 30s」部分的注释代码放开,事情就不一样了。

虚拟机的偏向锁实现里有两个很关键的东西:BiasedLockingDecayTime和revocation_count。

BiasedLockingDecayTime是开启一次新的批量重偏向距离上一次批量重偏向之后的延迟时间,默认为25000ms,这就是上面讲到的「规定的时间」。revocation_count是撤销计数器,会记录偏向锁撤销的次数。也就是说,在执行一次批量重偏向之后,经过了较长的一段时间(>=BiasedLockingDecayTime)之后,撤销计数器才超过阈值,则会重置撤销计数器。而是否执行批量重偏向和批量撤销正是依赖于撤销计数器的,sleep之后计数器被清零,本次不执行批量撤销,因此后续也就有机会继续执行批量重偏向。 

根据以上知识可知,等待一段时间后撤销计数器会清零,因此不会再执行批量撤销,而是变成再次执行批量重偏向。此时T3加锁的过程就和上面有所不同了,0~18号对象已经变为无锁,因此这部分只能加轻量级锁。关键是19~38号对象,从19号对象开始又会执行偏向锁撤销,到38号对象时刚好20次,这就绕回常规情况下T2执行时的场景了,T2执行时19号对象是不是从偏向T1变成了偏向T2?所以这里从38号对象开始往后的其他对象都会从T2重新偏向T3。

这里的特性用虚拟机里面的话讲叫做「启发式更新」,我理解这样做主要是出于性能上的考虑。假如偏向锁只是偶尔会发生轮流加锁的这种竞争,虚拟机是允许的,20次以内随便你怎么玩,可以一直帮你执行偏向锁撤销。如果25秒内撤销次数超过20次了,还友情提供一次批量重偏向。但是假如线程间竞争很多,频繁执行偏向锁撤销和批量重偏向则可能会比较损耗性能,因此「规定的时间」内连续撤销超过一定次数(默认40次)虚拟机就不让你偏向了,这就是批量撤销的意义所在。

大概就这些。

实验代码:

import org.openjdk.jol.info.ClassLayout;

import java.util.Vector;
import java.util.concurrent.locks.LockSupport;

/**
 * -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 -XX:+PrintCommandLineFlags
 */
public class TestBiased {

    static Thread T1, T2, T3;

    public static void main(String[] args) throws InterruptedException {
        test();
    }

    private static void test() throws InterruptedException {
        Vector<Doge> list = new Vector<>();

        int loopNumber = 39;
        T1 = new Thread(() -> {
            for (int i = 0; i < loopNumber; i++) {
                Doge d = new Doge();
                list.add(d);
                synchronized (d) {
                    System.out.println(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                }
            }
            LockSupport.unpark(T2);
        }, "T1");
        T1.start();

        T2 = new Thread(() -> {
            LockSupport.park();
            System.out.println("===============> ");
            for (int i = 0; i < loopNumber; i++) {
                Doge d = list.get(i);
                System.out.println(i + "\t" + ClassLayout.parseInstance(d).toPrintable());

                synchronized (d) {
                    System.out.println(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                }
                System.out.println(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
            }

            //sleep 30s
            /*try {
                Thread.sleep(30000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/

            LockSupport.unpark(T3);
        }, "T2");
        T2.start();

        T3 = new Thread(() -> {
            LockSupport.park();
            System.out.println("===============> ");
            for (int i = 0; i < loopNumber; i++) {
                Doge d = list.get(i);
                System.out.println(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                synchronized (d) {
                    System.out.println(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                }
                System.out.println(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
            }
        }, "T3");
        T3.start();

        T3.join();
        System.out.println(ClassLayout.parseInstance(new Doge()).toPrintable());
    }
}

class Doge {
}

参考资料:

https://www.bilibili.com/video/BV1jE411j7uX?p=85

https://blog.csdn.net/qq_36434742/article/details/106854061

https://github.com/farmerjohngit/myblog/issues/12

知乎:沈小洋

公众号:沈小洋

关于synchronized批量重偏向和批量撤销的一个小实验

 

本文地址:https://blog.csdn.net/shy_1023/article/details/109855335

相关标签: Java synchronized