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

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

批量重偏向和批量撤销


从偏向锁的加锁和解锁的过程中可以看出,当只有一个线程反复进入同步代码块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获取锁的时候,就需要等到 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


注意:这里建议共享对象为自定义类对象

相关文章
|
17天前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
29 4
|
17天前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
32 3
|
16天前
|
存储 算法 Java
深入理解Java虚拟机(JVM)及其优化策略
【10月更文挑战第10天】深入理解Java虚拟机(JVM)及其优化策略
31 1
|
4月前
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
38 0
|
26天前
|
存储 安全 Java
JVM锁的膨胀过程与锁内存变化解析
在Java虚拟机(JVM)中,锁机制是确保多线程环境下数据一致性和线程安全的重要手段。随着线程对共享资源的竞争程度不同,JVM中的锁会经历从低级到高级的膨胀过程,以适应不同的并发场景。本文将深入探讨JVM锁的膨胀过程,以及锁在内存中的变化。
25 1
|
28天前
|
监控 Java
Java的JVM如何优化?
Java的JVM如何优化?
51 3
|
14天前
|
存储 Kubernetes 架构师
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。
|
4月前
|
缓存 Prometheus 监控
Java面试题:如何监控和优化JVM的内存使用?详细讲解内存调优的几种方法
Java面试题:如何监控和优化JVM的内存使用?详细讲解内存调优的几种方法
90 3
|
4月前
|
监控 Java 中间件
FGC频繁导致CPU 飙升定位及JVM配置优化总结
FGC频繁导致CPU 飙升定位及JVM配置优化总结
132 0
|
4月前
|
存储 缓存 安全
Java面试题:介绍一下jvm中的内存模型?说明volatile关键字的作用,以及它如何保证可见性和有序性。
Java面试题:介绍一下jvm中的内存模型?说明volatile关键字的作用,以及它如何保证可见性和有序性。
34 0