偏向锁-批量重偏向和批量撤销测试

前言

最近看了一些synchronized底层原理的博文,对偏向锁这块儿不是很理解,结合网上一些博主的文章和示例代码,记录下自己的理解。

参考的文章有:
1.程序员囧辉的全网最硬核的 synchronized 面试题深度解析
2.Fisher3652的并发编程:批量重偏向、批量撤销

相关概念

锁升级流程:
在这里插入图片描述
对象头组成:
在这里插入图片描述

小端模式表示对象头

小端模式:Inter x86、ARM核 采用的是小端模式存储数据(低地址存放低字节数据,高地址存放高字节数据)

 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           05 01 00 00 (00000101 00000001 00000000 00000000) (261)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           ca d8 00 f8 (11001010 11011000 00000000 11111000) (-134162230)
     12     4   java.lang.String User.name                                 null

以上对象头中Mark word 4个字节的数据,是按照内存地址由低到高打印的,而内存中的地址是向上增长的,低地址存放低字节数据,高地址存放高字节数据,所以,Mark word实际的数据以16进制表示为:0x00000105

这样便与上图中对象头的 Mark word 的32位数据对应了起来,即锁标志位在低位。

启动偏向锁设置

JVM为我们提供了参数- XX:+UseBiasedLocking以开启或者关闭偏向锁优化(默认开启),但jvm延迟了4s启动偏向锁,想要在程序已启动时,就启动偏向锁,有两种方式:

  • 程序启动时,让主线程睡眠超过4s,这样就会在jvm延迟启动偏向锁后,才开始运行程序。
  • 设置jvm启动参数,添加-XX:BiasedLockingStartupDelay=0,即设置延迟启动偏向锁的时间为0s,这样jvm启动时,偏向锁就会启动。

批量重偏向

示例代码

创建20个锁对象,创建2个线程A、B,启动线程A,让线程B等待;在线程A中循环获取这20个锁对象;线程A执行完后,唤醒线程B,让线程B循环获取这20个已经偏向线程A的锁对象;

/**
 * 偏向锁-批量重偏向测试
 * @Author: AntonyZhang
 * @Date: 2021/7/6 10:25 下午
 **/
@Slf4j
public class BulkRebiasTest {

    static Thread A;
    static Thread B;
    static int loopFlag = 20;

    public static void main(String[] args) {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        final List<User> list = new ArrayList<>();
        A = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < loopFlag; i++) {
                    User a = new User();
                    list.add(a);
                    log.info("加锁前第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
                    synchronized (a) {
                        log.info("加锁中第 {} 次"+ ClassLayout.parseInstance(a).toPrintable(), i+1);
                    }
                    log.info("加锁结束第 {} 次"+ ClassLayout.parseInstance(a).toPrintable(), i+1);
                }
                log.info("============线程A 都是偏向锁=============");
                //防止竞争 执行完后唤醒线程B
                LockSupport.unpark(B);
            }
        };
        B = new Thread() {
            @Override
            public void run() {
                //防止竞争 先睡眠线程B
                LockSupport.park();
                for (int i = 0; i < loopFlag; i++) {
                    User a = list.get(i);
                    //因为从list当中拿出都是偏向线程A
                    log.debug("加锁前第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
                    synchronized (a) {
                        //20次撤销偏向锁偏向线程A;然后升级轻量级锁指向线程B线程栈当中的锁记录
                        //后面的发送批量偏向线程B
                        log.debug("加锁中第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
                    }
                    //因为前19次是轻量级锁,释放之后为无锁不可偏向
                    //但是第20次是偏向锁 偏向线程B 释放之后依然是偏向线程B
                    log.debug("加锁结束第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
                }
                log.debug("新产生的对象" + ClassLayout.parseInstance(new  User()).toPrintable());
            }
        };
        A.start();
        B.start();
    }
}

对于线程A,可以看到,20个锁对象的对象头中markword在对象创建后三个阶段的变化情况如下:
加锁前:都是处于可偏向状态
加锁中:线程A获取该锁对象后,锁对象的对象头中的markword字段 保存了线程A的线程ID
加锁后:每个锁对象依然偏向线程A,因为此时还未唤醒线程B
线程A打印如下图所示:
在这里插入图片描述

对于线程B,被唤醒后,前19个锁对象在加锁前、锁定中、释放后的三个阶段,对象头中的markword字段变化情况如下(前19次打印结果都一样,取第19次结果):
加锁前:偏向线程A
锁定中:先执行撤销偏向锁流程,然后执行轻量级锁加锁流程,加锁成功后,图中锁标志位为00
释放后:执行轻量级锁解锁流程
线程B打印如下图所示:
在这里插入图片描述

对于线程B,第20个锁对象被获取前、锁定中、释放后的三个阶段,对象头中的markword字段变化情况如下:
加锁前:偏向线程A
锁定中:先执行撤销偏向锁流程,偏向锁撤销计数器此时已累计撤销偏向20次,达到了批量重偏向的阈值(默认为20)。因此,便会触发批量重偏向的流程,重偏向后,锁对象偏向了线程B,图中锁标志位为101
释放后:依然偏向线程B
线程B如下图所示:
在这里插入图片描述

关于偏向锁的偏向纪元epoch,重偏向前后的变化情况,如下图中markword从低位到高位的
第8和第9个bit位所示:
可见,重偏向后,锁对象的epoch值为10,而重偏向前,锁对象的epoch值为00

重偏向前,偏向线程A的锁对象的markword
高位							    低位
01111010 00000100 00111000 00000101

重偏向后第20个锁对象的markword
11011101 00001100 10000001 00000101

重偏向后,新创建的锁对象的markword
00000000 00000000 00000001 00000101
过程总结

偏向锁的批量重偏向过程:
1.匿名偏向:开启偏向锁优化,类的实例对象创建后,尚未被任何一个线程拿到该锁对象,此时锁对象处于可偏向状态。

2.偏向锁:只有一个线程A进入了拿到锁进入同步块,此时,作为锁对象的对象头中markword 的偏向锁标志为1,锁标志位为01。

3.轻量级锁:线程A执行完同步块中任务,退出同步块后(此时,该锁对象依然偏向线程A),由线程B尝试获取步骤2中创建的锁对象,因为此时线程B属于第二个线程,即使线程A已经释放了锁,锁对象还是会撤销偏向并升级为轻量级锁。
1) 撤销偏向锁:线程B尝试获取锁,此时的对象锁依然为偏向锁,所以要执行偏向锁的撤销操作:CAS将锁对象的markword修改为无锁状态,撤销偏向后,偏向锁撤销计数器加1;
2)升级轻量级锁:锁对象的markword为无锁状态,先将锁对象的markword填充到栈中Lock Record的displaced_header字段,然后CAS将栈中Lock Record的地址记录到锁对象的对象头中markword的高30位;若CAS成功,则获取轻量级锁成功,此时对象头的markword锁标志位为00。

4.批量重偏向:先将Klass的prototype_header中的epoch值加1,然后遍历每个栈帧中包含的BasicObjectLock,如果其关联的锁对象oop是该Klass,则增加该oop的对象头中的epoch值,遍历完成后将触发批量重偏向的这个锁对象oop重新偏向给当前线程。

本例中在User类的20个对象的偏向锁都撤销偏向后,偏向锁撤销计数器的值为20,达到了偏向锁批量重偏向的阈值,就会触发批量重偏向。当前线程即为线程B。

批量撤销

示例代码

创建40个锁对象,创建2个线程A、B、C,线程A先执行,让线程B等待线程A执行结束后再执行,让线程C等待线程B执行结束后再执行;

在线程A中循环获取这40个锁对象;线程A执行完后,唤醒线程B,让线程B循环获取这40个已经偏向线程A的锁对象;

线程B执行完后,唤醒线程C,让线程C循环获取这40个锁对象(根据上面👆🏻批量重偏向的测试可知,此时的锁对象,前19个为无锁不可偏向,从第20个锁对象批量重偏向后,后21个为偏向线程B的锁对象)

/**
 * 偏向锁-批量撤销测试
 * @Author: AntonyZhang
 * @Date: 2021/7/6 10:25 下午
 **/
@Slf4j
public class BulkRevokeTest {

    static Thread A;
    static Thread B;
    static Thread C;
    static int loopFlag = 40;

    public static void main(String[] args) {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        final List<User> list = new ArrayList<>();
        A = new Thread(() -> {
            for (int i = 0; i < loopFlag; i++) {
                User a = new User();
                list.add(a);
                log.info("加锁前第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
                synchronized (a) {
                    log.info("加锁中第 {} 次"+ ClassLayout.parseInstance(a).toPrintable(), i+1);
                }
                log.info("加锁结束第 {} 次"+ ClassLayout.parseInstance(a).toPrintable(), i+1);
            }
            log.info("============线程A 都是偏向锁=============");
            //防止竞争 执行完后叫醒 线程B
            LockSupport.unpark(B);
        });
        B = new Thread(() -> {
            //防止竞争 先睡眠线程B
            LockSupport.park();
            for (int i = 0; i < loopFlag; i++) {
                User a = list.get(i);
                //因为从list当中拿出都是偏向线程A
                log.debug("加锁前第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
                synchronized (a) {
                    //40次撤销偏向锁偏向线程A;然后升级轻量级锁指向线程B线程栈当中的锁记录
                    //后面的发送批量偏向线程B
                    log.debug("加锁中第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
                }
                //因为前19次是轻量级锁,释放之后为无锁不可偏向
                //但是第20次是偏向锁 偏向线程B 释放之后依然是偏向线程B
                log.debug("加锁结束第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
            }
            log.debug("新产生的对象" + ClassLayout.parseInstance(new  User()).toPrintable());
            //防止竞争 执行完后叫醒 线程C
            LockSupport.unpark(C);
        });

        C = new Thread(() -> {
            //防止竞争 先睡眠线程C
            LockSupport.park();
            for (int i = 0; i < loopFlag; i++) {
                User a = list.get(i);
                log.debug("加锁前第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
                // 偏向撤销次数已达到批量撤销阈值40,则执行批量撤销流程
                synchronized (a) {
                    log.debug("加锁中第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
                }
                log.debug("加锁结束第 {} 次" + ClassLayout.parseInstance(a).toPrintable(), i+1);
            }
            log.debug("新产生的对象" + ClassLayout.parseInstance(new  User()).toPrintable());
        });

        A.start();
        B.start();
        C.start();
    }
}

线程B在升级为轻量级锁时,会对40个锁对象先执行偏向锁撤销流程,这样,而重偏向后,锁对象偏向线程B,偏向撤销计数器的值为40,达到了批量撤销的默认阈值,此时便会执行偏向锁的批量撤销流。

批量撤销:将 class 的 markword 修改为不可偏向无锁状态,也就是偏向标记位为0,锁标记位为01。接着遍历所有当前存活的线程的栈,找到该 class 所有正处于偏向锁状态的锁实例对象,执行偏向锁的撤销操作。

这样当线程后续尝试获取该 class 的锁实例对象时,会发现锁对象的 class 的 markword 不是偏向锁状态,知道该 class 已经被禁用偏向锁,从而直接进入轻量级锁流程。

线程C打印如下图:
可以看出,线程C在获取锁对象前,该锁对象已经是无锁不可偏向状态,锁定后,锁对象为轻量级锁,解锁后,恢复为无锁不可偏向状态;且新创建的锁对象仍然为无锁不可偏向状态,因为JVM任务该类的新实例对象已经不再适用偏向锁。
在这里插入图片描述

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
这是一个关于Java中锁的升级过程的问题。我会尽力解答,但是请注意我的回答可能会涉及一些技术术语。 Java中的锁分为四种级别:无锁、偏向锁、轻量级锁和量级锁。这些级别是根据竞争锁的线程数量和锁状态的不同而定义的。 1. 无锁:在无锁状态下,线程可以直接访问共享数据,不需要进行任何同步操作。这种情况只有在并发访问非常低的情况下才会出现,因为在高并发情况下,不同的线程很可能会同时访问同一块数据,这就需要进行同步处理。 2. 偏向锁:当只有一个线程访问共享数据时,可以使用偏向锁来提高性能。偏向锁是一种优化技术,它会记录下最后一个获取锁的线程,并在下一次访问时直接使用该线程持有的锁。这样可以减少锁的竞争,提高程序的性能。当有第二个线程访问同一块数据时,偏向锁就会升级为轻量级锁。 3. 轻量级锁:轻量级锁是一种比偏向锁更高级的锁机制。当两个线程访问同一块数据时,偏向锁就会升级为轻量级锁。轻量级锁使用CAS(Compare And Swap)操作来尝试获取锁,如果成功就直接持有锁,如果失败就会自旋等待。这种机制能够减少线程的上下文切换,提高程序的性能。当自旋次数超过一定值时,轻量级锁就会升级为量级锁。 4. 量级锁:量级锁是一种最基本的锁机制。当轻量级锁自旋次数超过一定值时,就会升级为量级锁。量级锁使用操作系统的互斥量来进行同步操作,这种机制需要进行线程的上下文切换,会降低程序的性能。 以上就是Java中锁的升级过程。在实际应用中,应该根据具体的情况选择不同的锁机制,以达到最优的性能表现。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值