【JUC】二十九、synchronized锁升级之轻量锁与重量锁

news/2024/5/17 18:51:54 标签: java, , synchronized, 锁消除, 锁粗化

文章目录

  • 1、轻量
  • 2、轻量的作用
  • 3、轻量的加和释放
  • 4、轻量级的代码演示
  • 5、重量级
  • 6、重量级的原理
  • 7、升级和hashcode的关系
  • 8、升级和hashcode关系的代码证明
  • 9、synchronized升级的总结
  • 10、JIT编译器对的优化:消除和粗化
  • 11、结语

📕相关笔记:

synchronized升级之 无
synchronized升级之 偏向

1、轻量

前面一篇提到偏向,即只有一个线程在竞争,此时通过资源类对象的对象头的Mark Word来标记,避免了用户态和内核态的频繁切换。

在这里插入图片描述

再往下,又来了一个线程也来竞争这个,且此时这两个线程近乎可以错开交替执行(或者说同步代码块/方法执行一次时间很短,哪怕另一个线程等,也不会等太久),如下图的1、2、3、4标号:

在这里插入图片描述

这就是轻量级的出现场景:有线程来参与竞争了,但不存在竞争太过激烈的情况,获取的冲突时间极端,本质就是CAS自旋,不要直接往重走。对应的共享对象内存图:

在这里插入图片描述

2、轻量的作用

轻量是为了在两个线程近乎交替执行同步块时来提高性能。

直白说就是先CAS自旋,不行了再考虑升级为重,使用操作系统的互斥量。升级到轻量的时机有:

  • 关闭了偏向
  • 多线程竞争偏向,可能导致偏向升级为轻量(这里写可能,是因为如果恰好是一个线程over,一个线程上位,则依旧是偏向

举个例子:比如现有A线程拿到了,A一个人走偏向玩了一会儿后,线程B来了,B在争抢时发现共享对象的对象头中Mark Word里的线程ID标记不是线程B的ID(而是线程A),此时,B线程通过CAS来尝试修改标记。当:

  • 此时线程A刚好Over,B上位,修改Mark Word里的线程ID为B,此时,仍为偏向,且偏向B

在这里插入图片描述

  • 如果A正在执行,B修改失败,则升级为轻量级,且轻量级继续由原来的线程A持有,接着执行刚才没执行完的,而线程B则自旋等待获取这个轻量级

在这里插入图片描述

3、轻量的加和释放

JVM会在线程的栈帧中创建用于存储记录Lock Record的空间,称为Displaced Mark Word。

在这里插入图片描述

若一个线程获得时发现是轻量级,会把对象的MarkWord复制到自己的Displaced Mak Word里面。然后线程尝试用CAS将的MarkWord替换为指向记录的指针。如下面两幅草图示意的变化过程:

在这里插入图片描述

在这里插入图片描述

如果替换成功,当前线程获得轻量。如果失败,表示Mark Word已经被替换成了其他线程的记录,说明在与其它线程竞争,当前线程就尝试使用自旋来获取(自旋一定次数后仍未获得,升级为重量)。

轻量级的释放:

在释放时,当前线程会使里CAS操作将Displaced Mark Word的内容复制回对象的Mark Word里面。如果没有发生竞争。那么这个复制的操作会成功。如果持有期间有其他线程因为自旋多次导致轻量级升级成了重量级,那么CAS操作会失败,此时会释放并唤醒被阻塞的线程。

4、轻量级的代码演示

java">-XX:-UseBiasedLocking

添加JVM参数,关闭偏向,就可以直接进入轻量级

java">Object object = new Object();
new Thread(() -> {
    synchronized (object){
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
}).start();

运行:

在这里插入图片描述

轻量下,自旋达到一定次数或者说程度,会升级为重量

  • Java6之前,默认情况下自旋的次数是10次或者自旋的线程数超过了cpu核数的一半,可-XX:PreBlockSpin=10来修改
  • Java6之后,JVM做了优化,采用自适应自旋

自适应自旋,即线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率会成功。反之,如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,以避免CPU空转。直白说就是会总结前人的经验了、会预判走位了。

轻量与偏向的区别:

  • 偏向是一个线程自己在玩,而偏向涉及竞争,且争夺轻量级失败时,自旋尝试抢占
  • 轻量级每次退出同步块都需要释放(要不就不会是一个走了一个接上了),而偏向则只在有线程来竞争时才释放

5、重量级

竞争太激烈时,只能捅到重量级,进行内核态和用户态的切换,但前面偏向和轻量级已然做了一定程度的缓冲和优化了。

在这里插入图片描述

有大量的线程参与的竞争,冲突性很高:

java">Object object = new Object();
//多个线程
for (int i = 0; i < 6; i++) {
    new Thread(() -> {
        synchronized (object){
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
    },String.valueOf(i)).start();
}

运行:

在这里插入图片描述

6、重量级的原理

Java中synchronized的重量级,是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitor enter指令,在结束位置插入monitor exit指令。

当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了,会在Monitor的owner中存放当前线程的id,这样它将处于定状态,除非退出同步块,否则其他线程无法获取到这个Monitor。

7、升级和hashcode的关系

在这里插入图片描述

可以看到,无状态下,Java对象头的Mark Word中是有空间存hashcode的,升级后,则没有位置了,那要是升级后hashcode去哪儿了 ?

在这里插入图片描述

总结下:

1) 在无状态下,Mark Word可以存储对象的identity hash code值,当对象的hashCode()方法第一次被调用时,JVM会生成对应的identity hash code值,存于对象头的Mark Word中。

2) 对于偏向,在线程获取偏向时,用Thread Id和epoch值(看成时间戳)去覆盖identity hash code所在的位置。如果一个对象的hashcode()方法已经被调用过一次,则这个对象不能被设置偏向,因为如果可以,那identity hash code就会被线程ID覆盖,就会造成同一对象,前后两次调用hashcode方法得到的结果不一致。

3) 升级为轻量,JVM会在当前线程的栈帧中创建一个记录空间Lock Record(前面已提到),用于拷贝和存储对象的Mark Word,里面自然包含了identity hash code、GC年龄等,且释放轻量时,这些数据又会写回对象头,因此轻量级可以和identity hash code共存。

4) 到重量级,Mark Word保存的是重量级指针,而代表重量级的ObiectMonitor类里有字段记录了非加状态下的Mark Word,释放以后也会写回对象头。

8、升级和hashcode关系的代码证明

Case1:当一个对象已经计算过identity hashcode,它就无法进入偏向状态,会跳过偏向,直接升级轻量级

java">//先睡5秒,抵消偏向开启的延时,保证开启偏向
TimeUnit.SECONDS.sleep(5);
Object object = new Object();
System.out.println("这里应该是偏向==>");
System.out.println(ClassLayout.parseInstance(object).toPrintable());
//没有重写hashcode,重写后无效
int hashCode = object.hashCode();
//验证当一个对象已经计算过identity hash code后,就无法进入偏向状态
new Thread(() -> {
    synchronized (object){
        System.out.println("这里本应是偏向,但刚才计算过一致性哈希hashcode,这里会直接升级为轻量级 ==>");
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
}).start();

在这里插入图片描述

Case2:偏向过程中遇到一致性哈希计算请求,立马撤销偏向模式,膨胀为重量级

java">//先睡5秒,抵消偏向开启的延时,保证开启偏向
TimeUnit.SECONDS.sleep(5);
Object object = new Object();
synchronized (object){
    System.out.println(ClassLayout.parseInstance(object).toPrintable());
    System.out.println("此时是偏向,但下面一计算哈希,会立马撤销偏向模式,膨胀为重量级");
    //计算哈希值,这里的hashcode方法是没有重写过的
    int hashCode = object.hashCode();
    System.out.println(ClassLayout.parseInstance(object).toPrintable());
}

在这里插入图片描述

synchronized_203">9、synchronized升级的总结

在这里插入图片描述

synchronized升级,目的还是实现一个性能优化,思想就是:先自旋,不行了再阻塞。一直都是围绕尽量避免内核态和用户态频繁切换来展开的。实际上是把之前的悲观(重量级)变成在一定条件下使用偏向以及使用轻量级(自旋CAS)的形式。太精辟了这句!道出了这几种的关系。

在这里插入图片描述

另外,synchronized在修饰方法和代码块时,在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MarkWord来实现的。JDK1.6之前synchronized使用的是重量级,JDK1.6之后进行了优化,拥有了无->偏向->轻量级->重量级的升级过程,而不是无论什么情况都使用重量级

请添加图片描述
最后 :

  • 偏向:适用于单线程的情况,在不存在竞争的时候进入同步方法/代码块则使用偏向
  • 轻量级:适用于竞争较不激烈的情况(这和乐观的使用范围类似),轻量级采用的是自旋如果同步方法/代码块执行时间很短的话(就很容易一个线程完事儿了,另一个线程尚未,哪怕不是这么刚刚好,也自旋等不了太久),采用轻量级自旋虽然会占用Cpu资源,但是相对比使用重量级还是更高效。
  • 重量级:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级自旋带来的性能消耗就比使用重量级一更严重,这时候就需要升级为重量级

10、JIT编译器对的优化:消除和粗化

JIT,即Just Time Compiler,翻译:即时编译器。

synchronized消除

以下是一个简单的synchronized代码,没啥毛病(别说优化成线程池):

java">public class LockClearDemo {

    static Object objectLock = new Object();

    public void m1(){
        synchronized (objectLock){
            System.out.println("----hello clearDemo");
        }
    }
    public static void main(String[] args) {
        LockClearDemo lockClearDemo = new LockClearDemo();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                lockClearDemo.m1();
            },String.valueOf(i)).start();
        }
    }
}

但此时,做出这样一个修改:

在这里插入图片描述

这么写,看似有synchronized,语法也没报错,实际每个线程进来都new一个自己的object对象,相当于是每一个线程一个自己创造的,而不是正常的所有线程共同抢一个对象的,因此,这么写毫无意义,JIT编译器会无视它,极端的说就是根本没有加这个对象的底层的机器码,是消除了的使用。

synchronized粗化

看示例代码:

java">public class LockDemo {

    static Object objectLock = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (objectLock){
                System.out.println("111111");
            }
            synchronized (objectLock){
                System.out.println("222222");
            }
            synchronized (objectLock){
                System.out.println("333333");
            }
            synchronized (objectLock){
                System.out.println("444444");
            }
        }).start();
    }
    
}

注意,这不是可重入,这里是频繁加。虽然无语法错误,但底层编译器会把它合并优化为:

在这里插入图片描述

粗化:假如方法中首尾相接,前后相邻的都是同一个对象,那JIT编译器就会把这几个synchronized块合并成一个大块,加粗加大范围,一次申请使用即可,避免次次的申请和释放,提升了性能。

11、结语

  • 没有:自由自在
  • 偏向:唯我独尊
  • 轻量:楚汉争霸
  • 重量:群雄逐鹿

http://www.niftyadmin.cn/n/5263730.html

相关文章

计算机设计大赛信息可视化设计的获奖经验剖析解读—基于本专栏文章助力4C大赛【全网最全万字攻略-获奖必读】

文章目录 一.中国大学生计算机设计大赛1.1赛道解读1.2 信息可视化设计小类介绍1.2 小类区别解读 二.信息可视化设计赛道获奖经验2.1 四小类作品预览2.1.1 数据可视化小类-优秀参赛作品展览2.1.2 信息图形设计小类-优秀参赛作品展览2.1.3 动态信息影像&#xff08;MG动画&#x…

架构简洁之道有感,谈谈软件组件聚合的张力

配图由腾讯混元助手生成 这篇文章介绍了软件架构设计中组件设计思想&#xff0c;围绕“组件间聚合的张力”这个有意思的角度&#xff0c;介绍了概念&#xff0c;并且结合架构设计示例对这个概念进行了进一步阐述。 组件聚合&#xff1f;张力&#xff1f;这标题&#xff0c;有种…

bugkuctf web随记wp

常规思路&#xff1a; 1&#xff0c;源码2&#xff0c;抓包3&#xff0c;御剑dirsearch扫后台检查是否有git文件未删除4&#xff0c;参数 本地管理员&#xff1a;1&#xff0c;cu看源码&#xff0c;sci看源码有一串东西2&#xff0c;base64解码后是test123猜测是密码3&#x…

了解如何在linux使用podman管理容器

本章主要介绍使用 podman 管理容器。 了解什么是容器&#xff0c;容器和镜像的关系 安装和配置podman 拉取和删除镜像 给镜像打标签 导出和导入镜像 创建和删除镜像 数据卷的使用 管理容器的命令 使用普通用户管理容器 使用普通用户管理容器 对于初学者来说&#xff0c;不太容…

解决PP材质粘合问题用PP专用UV胶水

PP材料已经广泛应用于各行各业&#xff0c;在粘接中会有不同的问题需求&#xff0c;那么使用专用于PP的UV胶水可能是解决PP材质粘合问题的一种有效方法。 主要在于&#xff1a;UV胶水在紫外线照射下可以快速固化&#xff0c;形成坚固的连接。所以使用PP专用UV胶水时可以考虑&am…

HarmonyOS--基础组件Button

Button组件 可以包含单个子组件。 Button(label?: ResourceStr, options?: { type?: ButtonType, stateEffect?: boolean }) 1&#xff1a;文字按钮 Button(‘点击’) 2&#xff1a;自定义按钮,嵌套其它组件 Button() {Image(https://) }.type(ButtonType.Circle)

2-2基础算法-Nim和/前缀和/差分

文章目录 一.Nim和二.前缀和&区间和三.差分 一.Nim和 Nim游戏是一个数学策略游戏&#xff0c;通常涉及两名玩家轮流从几堆物品&#xff08;如石子或饼干&#xff09;中取走一定数量的物品。每个玩家每次可以从任意一堆中取走任意数量的物品&#xff0c;但必须至少取走一个…

springoot集成kafka

1.常见两种模式 2.高可用 和 负载均衡 组内:消费者 一个只能消费一个分区 组外:消费者消费是订阅者模式