面试加分项:JVM 锁优化和逃逸分析详解

(给ImportNew加星标,提高Java技能)


1 锁优化


JVM 在加锁的过程中,会采用自旋、自适应、锁消除、锁粗化等优化手段来提升代码执行效率。


1.1 自旋锁和自适应自旋


现在大多的处理器都是多核处理器 ,如果在多核心处理器,有让两个或者以上的线程并行执行,我们可以让一个等待线程不放弃处理器的执行时间。设置一个等待超时时间,看线程是否能够很快的释放锁,在等等待的这段时间可以执行一个空循环,让当前线程继续占用 CPU 的时间片。这就是所谓的「自旋锁」。


JVM 中可以通过 +XX:UseSpinning来开启自旋锁,在 JDK1.6 过后默认为我们开启。由于自旋锁的使用会让锁的竞争者占用更多的处理器时间, JVM 规定了一个自旋次数的一个参数。我们可以通过 -XX:PreBlockSping来进行更改(默认10次)。


偏向锁、轻量级锁的状态转化及对象 Mark Word 的关系转换入下图所示:


偏向锁、轻量级锁的状态转化及对象 Mark Word 的关系


1.2 锁消除


锁消除是指虚拟机即时编译器在运行时检测到某段需要同步的代码不可能存在共享数据竞争而实施的一种对锁进行消除的优化策略。锁消除的主要判断依据于逃逸分析。如果判断一段代码,在堆上所有的数据都不会逃逸出去被别的线程访问到,那就把它当作栈上的数据对待,认为它们是私有的,同步加锁就无需进行。


下面是三个字符串 x, y, z 相加的例子,无论是从源代码上还是逻辑上都没有进行同步:


public String concatStr(String x, String y, String z) {    return  x + y + z;}

   


String 是一个不可变的类,对字符的链接总是生成新的 String 对象来进行的,因此 Javac 编译器会对 String 链接进行自动优化,在 JDK5 之前字符串链接会转换为 StringBuffer;在 JDK5 之后会转换为 StringBuilder 对象连续的 append()操作,我们看看 javac 过后,反编译的结果:


public String concatStr(String x, String y, String z) {    StringBuilder sb = new StringBuilder();    sb.append(x);    sb.append(y);    sb.append(z);    return  sb.toString();}

   

我们再来看看 javap 反编译的结果:


javap 反编译的结果


这里大家可能会担心 StringBuilder 不是线程安全的的操作会存在线程安全的问题吗?这里的答案是不会,x + y + z 操作的优化「经过逃逸分析」过后,他的动态作用域被限制在了 concatStr方法内,就是说当前实际执行的 StringBuilder 的操作在 concatStr 方法内部,「其他的外部线程无法访问」到,所以这里「虽然有锁,但是可以被安全的消除掉。所以当我们进行编译过后,这段代码就会忽略掉所有的同步措施直接执行。」


1.3 锁粗化


原则上,我们在写代码的时候,总是推荐将同步块的作用范围限制得尽可能的小--只在共享数据的实际操作作用域中才进行同步,这样也是为了使得需要同步的操作尽可能的变少,即使存在锁的竞争,等待的锁的线程也能很快的获取到锁。大多数情况下,上面的原则都是正确的,但是如果「一系列的连续操作都是对同一个对象反复加锁和解锁,甚至加锁操作时出现在循环体之中」的,那即使没有线程的竞争,频繁的进行相互操作也会导致不必需要的性能损耗。


StringBuffer buffer = new StringBuffer();/**  锁粗化 */ public void append(){    buffer.append("aaa").append(" bbb").append(" ccc"); }

   


上面的代码每次调用 buffer.append 方法都需要加锁和解锁,如果 JVM 家册到有一串连续的对同一个对象加锁和解锁的操作,就会将其合并成一次范围更大的加锁解锁操作,即在第一个 append 方法执行的时候进行加锁,最后一个 append 方法结束后进行解锁。


2 逃逸分析


逃逸分析(Escape Analysis),是一种可能减少有效 Java 程序中同步负载和内存堆分配压力的跨全局函数数据流分析算法。通过逃逸分析, Java Hotspot 编译器能够分析出一个新的对象引用范围从而决定是否要将这个对象分配到堆上,「逃逸分析的基本行为就是分析对象的动态作用域。」


2.1 方法逃逸


当一个对象在方法里面被定义后,它可能被外部方法所引用,例如调用参数传递到其他方法中,这种称为方法逃逸。


2.2 线程逃逸


当一个对象可能被外部线程访问到,比如:赋值给其他线程中访问的实例变量,这种称为线程逃逸。


2.3 通过逃逸分析,编译器对代码的优化


如果能够证明一个对象不会逃逸到到方法外或线程外(其他线程方法或者线程无法通过任何方法访问该变量),或者逃逸程度比较低(只逃逸出方法而不逃逸出线程)则可以对这个对象采用不同程度的优化:


  1. 栈上分配(Stack Allocations)完全不会逃逸的局部变量和不会逃逸出线程的对象,采用栈上分配,对象就会跟随方法的结束自动销毁。以减少垃圾回收器的压力。

  2. 标量替换(Scalar Replacement)有个对象可能不需要作为一个连续的存储结果存储也能被访问到,那么对象的部分(或者全部)可以不存储在内存,而是存储在 CPU 寄存器中。

  3. 同步消除(Synchronization Elimination)如果一个对象发现只能在一个线程访问到,那么这个对象的操作可以考虑不同步。


参考资料

  • https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html

  • https://www.cnblogs.com/xidongyu/p/10891303.html

  • https://www.cnblogs.com/kkkkkk/p/5543799.html


转自:心城以北,

链接:juejin.cn/post/7055891706826194975


- EOF -

推荐阅读  点击标题可跳转

1、JVM 中的逃逸分析

2、死锁问题排查过程-间隙锁的复现以及解决

3、20 张图带你彻底了解 ReentrantLock 加锁解锁的原理


看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

点赞和在看就是最大的支持❤️


相关推荐

  • Kotlin 与 Java 如何解决 Null 问题?
  • ofo小黄车已彻底无法登录;初代 iPhone 拍出 6.3 万美元天价;Linux 6.3 合并了硬件噪声工具|极客头条
  • 推荐一些基于 ChatGPT 的开源项目
  • Spring Boot 使用 ChatGPT API 开发一个聊天机器人
  • 年初面试,有点刺激了这波。。。
  • 通过 contextlib 模块详细复习一下 with 语句的用法
  • 聊聊Nacos
  • Win 11 新功能
  • 对ChatGPT的观察与思考
  • AI自给自足!用合成数据做训练,效果比真实数据还好丨ICLR 2023
  • 车万翔:ChatGPT时代,NLPer 的危与机
  • IM专题:分层架构IM系统(9)— Das核心职责和逻辑设计
  • 医院弄错试管婴儿让家长别计较;日本高端外国人才66%来自中国;公务员省考有省份扩招近 80%......|酷玩日爆
  • 什么男人一眼看上去很穷? ​| 每日一冷
  • “医保个账减少,增加5000家药店有何用?”
  • 宁德时代疯狂降价,电车们终于可以打骨折了?
  • 你想在价值10万元的床垫上滚床单吗?
  • 逼死代购!排队2小时才能买到的盖章版《紫禁城100》,我们原价包邮卖
  • 流浪地球周边到!2800+零件还原装甲车,科幻迷沸腾了!
  • 拜登秘密抵达乌克兰基辅;慈善主播在凉山向老人借钱摆拍“发钱”;俄亥俄一所金属工厂发生爆炸| 每日大新闻