Java synchronized那点事

本文详细探讨了Java中的synchronized关键字,特别是偏向锁、轻量级锁和重量级锁的工作原理。介绍了偏向锁的重偏向、撤销和匿名偏向,以及轻量级锁的加锁、解锁过程。讨论了锁升级的必要性和优缺点,强调在多线程竞争激烈时的性能问题。此外,还提到了重量级锁的实现、monitor的初始化和工作流程。最后,文章提供了关于自旋锁、锁粗化和锁消除等优化技术的见解,并提醒读者注意性能调优的选择和取舍。
摘要由CSDN通过智能技术生成

锁粗化过程

偏向锁

①:markword中保存的线程ID是自己且epoch等于class的epoch,则说明是偏向锁重入。

②:偏向锁若已禁用,进行撤销偏向锁。

③:偏向锁开启,都进行进行重偏向操作。

④:若进行了锁撤销操作或重偏向操作失败,则需要升级为轻量级锁或者进一步升级为重量级锁。

匿名偏向

锁对象在发送锁竞争后会升级为偏向锁,不过当不发生锁竞争时,锁对象依然会升级为偏向锁,这种情况叫匿名偏向。

当jvm启动4s后,会默认给新建的对象加上偏向锁。

上代码:

<dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.8</version>
        </dependency>

这个包下的工具类的功能有:

// 查看对象内部结构
         System.out.println(ClassLayout.parseInstance(bingo).toPrintable());
         // 查看对象外部信息
         System.out.println(GraphLayout.parseInstance(bingo).toPrintable());
         // 查看对象总大小
         System.out.println(GraphLayout.parseInstance(bingo).totalSize());

默认JVM是开启指针压缩,可以通过vm参数开启关闭指针压缩: -XX:-UseCompressedOops 。

当创建锁对象前不进行休眠4s的操作:

@Test
    public void mark() throws InterruptedException {
        Bingo bingo = new Bingo();
        bingo.setP(1);
        bingo.setB(false);
        // 查看对象内部结构
        System.out.println(ClassLayout.parseInstance(bingo).toPrintable());
        System.out.println("\n++++++++++++++++++++++++++\n");
        synchronized (bingo) {
            // 查看对象内部结构
            System.out.println(ClassLayout.parseInstance(bingo).toPrintable());
        }
    }

看我标红线的后三位的值,由于启动过快,锁直接从无锁升级成了轻量级锁。

当创建锁对象前进行休眠4s的操作:

@Test
    public void mark() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);

        Bingo bingo = new Bingo();
        bingo.setP(1);
        bingo.setB(false);
        // 查看对象内部结构
        System.out.println(ClassLayout.parseInstance(bingo).toPrintable());
        System.out.println("\n++++++++++++++++++++++++++\n");
        synchronized (bingo) {
            // 查看对象内部结构
            System.out.println(ClassLayout.parseInstance(bingo).toPrintable());
        }
    }

当在程序启动4s后创建锁对象,就会默认偏向。

重偏向

因为偏向锁不会自动释放,因此当锁对象处于偏向锁时,另一个线程进来只能依托VM判断上一个获取偏向锁的线程是否存活、是否退出持有锁来决定是锁升级还是进行重偏向。

锁撤销

①:偏向锁的撤销必须等待VM全局安全点(安全点指所有java线程都停在安全点,只有vm线程运行)。

②:撤销偏向锁恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态。

③:只要发生锁竞争,就会进行锁撤销。

备注:

当开启偏向锁时,若持有偏向锁的线程仍然存活且未退出同步代码块,锁升级为轻量级锁/重量级锁之前会进行偏向锁撤销操作。

如果是升级为轻量级锁,撤销之后需要创建Lock Record 来保存之前的markword信息。

批量偏向/撤销概念:

参考1: 盘一盘 synchronized (二)—— 偏向锁批量重偏向与批量撤销 - 柠檬五个半 - 博客园

  • 批量重偏向
    当一个线程同时持有同一个类的多个对象的偏向锁时(这些对象的锁竞争不激烈),执行完同步代码块后,如果另一个线程也要持有这些对象的锁,当对象数量达到一定程度时,会触发批量重偏向机制(进行过批量重偏向的对象不可再进行批量重偏向)。
  • 批量锁撤销
    当触发批量重偏向后,会触发批量撤销机制。

阈值定义在globals.hpp中:

// 批量重偏向阈值
  product(intx, BiasedLockingBulkRebiasThreshold, 20)
  // 批量锁撤销阈值
  product(intx, BiasedLockingBulkRevokeThreshold, 40)

可以在VM启动参数中通过 -XX:BiasedLockingBulkRebiasThreshold 和 -XX:BiasedLockingBulkRevokeThreshold 来手动设置阈值。

偏向锁的撤销和重偏向的代码(过于复杂)在biasedLocking.cpp中:

void BiasedLocking::revoke_at_safepoint(Handle h_obj) {
  assert(SafepointSynchronize::is_at_safepoint(), "must only be called while at safepoint");
  oop obj = h_obj();
  HeuristicsResult heuristics = update_heuristics(obj, false);
  if (heuristics == HR_SINGLE_REVOKE) {
    // 重偏向
    revoke_bias(obj, false, false, NULL);
  } else if ((heuristics == HR_BULK_REBIAS) ||
             (heuristics == HR_BULK_REVOKE)) {
    // 批量撤销或重偏向
    bulk_revoke_or_rebias_at_safepoint(obj, (heuristics == HR_BULK_REBIAS), false, NULL);
  }
  clean_up_cached_monitor_info();
}

参考2:

对于存在明显多线程竞争的场景下使用偏向锁是不合适的,比如生产者-消费者队列。生产者线程获得了偏向锁,消费者线程再去获得锁的时候,就涉及到这个偏向锁的撤销(revoke)操作,而这个撤销是比较昂贵的。那么怎么判断这些对象是否适合偏向锁呢?jvm采用以类为单位的做法,其内部为每个类维护一个偏向锁计数器,对其对象进行偏向锁的撤销操作进行计数。当这个值达到指定阈值的时候,jvm就认为这个类的偏向锁有问题,需要进行重偏向(rebias)。对所有属于这个类的对象进行重偏向的操作叫批量重偏向(bulk rebias),之前的做法是对heap进行遍历,后来引入epoch。当需要bulk rebias时,对这个类的epoch值加1,以后分配这个类的对象的时候mark字段里就是这个epoch值了,同时还要对当前已经获得偏向锁的对象的epoch值加1,这些锁数据记录在方法栈里。这样判断这个对象是否获得偏向锁的条件就是:mark字段后3位是101,thread字段跟当前线程相同,epoch字段跟所属类的epoch值相同。如果epoch值不一样,即使thread字段指向当前线程,也是无效的,相当于进行过了rebias,只是没有对对象的mark字段进行更新。如果这个类的revoke计数器继续增加到一个阈值,那个jvm就认为这个类不适合偏向锁了,就要进行bulk revoke。于是多了一个判断条件,要查看所属类的字段,看看是否允许对这个类使用偏向锁。

轻量级锁

轻量级体现在线程会尝试在自己的堆栈中创建Lock Record存储锁对象的相关信息,不需要在内核态和用户态之间进行切换,不需要操作系统进行调度。

加锁

拿到轻量级锁线程堆栈:

Lock Record主要分为两部分:

  • obj
    指向锁对象本身。重入时也如此。
  • displaced header(缩写为hdr)
    第一次拿到锁时hdr存放的是encode加密后的markword,重入时存放null。

思考:为什么锁重入时hdr存放的是null,而不是用计数器来实现呢?

假设一个场景,当一个线程同时拿到A、B、C...N 多个锁的时候,那么线程的堆栈中,肯定有多个锁对象的Lock Record,

如:

synchronized(a){
    synchronized(b){
        synchronized(c){
            // do something
            synchronized(a){
                // do something
            }
        }
    }
}

当锁a重入时,如果用计数器,还得遍历当前线程堆栈拿到第一次的Lock Record,解锁时也要遍历,效率必然低下。作为jdk底层代码必然讲究效率。

以上纯属个人看法(欢迎交流)。

解锁

①:使用遍历方式将当前线程堆栈中属于该锁对象的Lock Record 指向Null。

②:CAS还原markword为无锁状态。

③:第②步失败需要升级为重量级锁。

优缺点

  • 优点

    在线程接替/交替执行的情况下,锁竞争比较小,可以避免成为重量级锁而引起的性能问题。

  • 缺点

    当锁竞争比较激烈、多线程同事竞争锁的时候,需要从轻量级升级为重量级,产生了额外的开销。

源码分析

加锁

加锁、解锁流程的代码在InterpreterRuntime.cpp中。

这是我从github拉下来的源码:

/**
       * (轻量级锁࿰
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值