前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >啥?小胖连 JVM 对锁做了那些优化都不知道?真的菜!

啥?小胖连 JVM 对锁做了那些优化都不知道?真的菜!

作者头像
JavaFish
发布2021-01-18 14:36:08
4970
发布2021-01-18 14:36:08
举报

来到多线程的第十五篇,对前十四篇感兴趣的请点文末底部的上、下一篇标签。这篇来聊聊 JVM 对 synchronized 做了那些优化?

JDK1.6 之前,synchronized 一直被认为是重量级锁。而在 JDK1.6 之后,JVM 对 synchronized 内置锁的性能进行了很多优化,包括「自适应的自旋锁、锁消除、锁粗化、偏向锁、轻量级锁」等等。加了这些优化之后,synchronized 锁的性能得到了大幅度的提升,下面我们来瞧瞧到底咋回事?

自适应的自旋锁

什么是自旋?字面意思是 "自我旋转" 。在 Java 中也就是循环的意思,比如 for 循环,while 循环等等。那自旋锁顾名思义就是「线程不放过 CPU,一直循环地去获取锁,直至获取到才去执行任务,否则一直在自旋」

上一章聊自旋锁的时候,我们知道 AtomicXxx 类都是 java 自旋锁的体现。通过 AtomicInteger 源码来回忆一下,自旋锁的原理:

代码语言:javascript
复制
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

代码中使用一个 do-while 循环来一直尝试修改 int 的值。自旋的缺点在于如果自旋时间过长,那么性能开销是很大的,浪费了 CPU 资源。

JDK1.6 之前可以使用 「-XX:+UseSpinning」 来开启自旋锁,在 JDK1.6 之后默认开启。同时自旋的默认次数为 10 次,可以通过参数 「-XX:PreBlockSpin」 来调整次数,但会带来诸多的不便。

比如:我设置为 10 次,但系统中很多线程都是等自旋线程刚退出的时候就释放锁(加入自旋线程多旋 1、2 次就能成功几个获取锁),这个时候就很尴尬了。首先我没办法判断我要自旋多少次。

所以,在 「JDK 1.6」 中引入了自适应的自旋锁来解决这个问题。自适应意味着自旋的时间不再固定,而是会根据最近自旋尝试的「 成功率、失败率,以及当前锁的拥有者的状态」等多种因素来共同决定。具体规则如下:

  • 自旋次数通常由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。如果「线程 A」 自旋成功,自旋次数为 17 次,那么等到下一个「 线程 B」 自旋时,也会默认认为「 线程 B」 自旋 17 次成功,
  • 如果「线程 B」 自旋了 5 次就成功了,那么此时这个自旋次数就会缩减到 5 次。

自适应自旋锁随着程序运行和性能监控信息,从而「使得虚拟机可以预判出每个线程大约需要的自旋次数」

锁消除

在聊锁消除之前,可能得先聊两个概念,一个叫 「JIT」,一个叫「 逃逸分析」。本文只简单介绍下他们的概念,具体的原理,篇幅原因就不说了。以后单独写一篇来聊。

  • JIT(Just In Time)即时编译器,是一种优化手段。「JVM 在编译时会发现某个方法或代码块运行特别频繁的时候,就会认为这是 “热点代码”(Hot Spot Code)。然后 JIT 会把部分 "热点代码" 翻译成本地机器相关的机器码,并进行优化,然后再把翻译后的机器码缓存起来,以备下次使用」。HotSpot 虚拟机中内置了两个 JIT 编译器:Client Complier 和 Server Complier。
  • 逃逸分析:一种可以有效减少 Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot 编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

简单来说,它的基本行为就是:「当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸」。我们最常见的就是静态变量 (类变量) 赋值,称为线程逃逸

在 Java 代码运行时,通过 JVM 参数可指定是否开启逃逸分析:

  • -XX:+DoEscapeAnalysis :表示开启逃逸分析
  • -XX:-DoEscapeAnalysis :表示关闭逃逸分析 从 jdk 1.7 开始已经默认开始逃逸分析,如需关闭,需要指定 - XX:-DoEscapeAnalysis

举个栗子:

代码语言:javascript
复制
/**
 * 方法一
**/
public static StringBuffer craeteStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb;
}

/**
 * 方法二
**/
public static String createStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}

如上,方法一中的 sb 就逃逸了,而方法二中的 sb 并没有逃逸,因为方法二的 sb 对象的作用域只在方法内,而法一直接把对象返回去调用方了。

逃逸分析可以对代码做 3 个优化:同步省略、将堆分配转化为栈分配以及分离变量或标量替换。「同步省略就是我们常说的锁消除」

举个例子:下面方法,我想打印一个对象,我担心出现线程安全问题,加了个锁。

代码语言:javascript
复制
public void print() {
    Object dog = new Object();
    synchronized(dog) {
        System.out.println(dog);
    }
}

但是 JIT 编译时借助逃逸分析发现 dog 对象的生命周期只在 print 方法中,并不会被其他线程所访问到,所以在 JIT 编译阶段就会被优化掉。优化成:

代码语言:javascript
复制
public void print() {
    Object dog = new Object();
    System.out.println(dog);
}

「所以,在使用 synchronized 的时候,如果 JIT 经过逃逸分析之后发现并无线程安全问题的话,就会做锁消除」

锁粗化

锁粗化,「如果我们释放了锁,紧接着什么都没做又或者做一些不需要同步且耗时很短的操作,又重新获取锁」。代码如下:

代码语言:javascript
复制
public void lockCoarsening() {

    synchronized(this) {
        //do something
    }

    synchronized(this) {
        //do something
    }

    synchronized(this) {
        //do something
    }

}

你也发现了,其实这种释放和重新获取锁是完全没有必要的。JVM 会这么优化:把同步的区域扩大,尽量避免不必要的加解锁操作。

代码语言:javascript
复制
public void lockCoarsening() {

    synchronized(this) {
        //do something
        //do something
        //do something
    }

}

有经验的朋友可能会想到在循环的场景下,有如下代码:第一段代码会被优化成第二段代码的样子。这时就要注意了,这个循环的耗时是非常长的吗?如果是,这就会导致其他线程长时间无法获得锁。「所以,这里的锁粗化不适用于循环的场景,仅适用于非循环的场景」

代码语言:javascript
复制
for (int i = 0; i < 1000; i++) {

    synchronized(this) {
        //do something
    }

}

//优化
synchronized(this) {
    for (int i = 0; i < 1000; i++) {
        //do something
    }
}

偏向锁 / 轻量级锁 / 重量级锁

介绍一下偏向锁、轻量级锁和重量级锁,这三是指 synchronized 锁的状态,通过在对象头中的 mark word 来表明锁的状态。

  • 偏向锁

❝一个对象在被初始化后,如果还没有任何线程来获取它的锁时,它就是可偏向的,当有第一个线程来访问它尝试获取锁的时候,它就记录下来这个线程,如果后面尝试获取锁的线程正是这个偏向锁的拥有者,就可以直接获取锁,开销很小。 ❞

  • 轻量级锁

❝synchronized 中的代码块是被多个线程交替执行的,也就是说,并不存在实际的竞争,或者是只有短时间的锁竞争,用 CAS 就可以解决。这种情况下,重量级锁是没必要的。轻量级锁指当锁原来是偏向锁的时候,被另一个线程所访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的方式尝试获取锁,不会阻塞。 ❞

  • 重量级锁

❝利用操作系统的同步机制实现,所以开销比较大。当多个线程直接有实际竞争,并且锁竞争时间比较长的时候,此时偏向锁和轻量级锁都不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。 ❞

锁升级的路径

根据前面的描述,我们知道:「偏向锁性能最好,避免了 CAS 操作。而轻量级锁利用自旋和 CAS 避免了重量级锁带来的线程阻塞和唤醒,性能中等。重量级锁则会把获取不到锁的线程阻塞,性能最差」。所以,JVM 默认优先使用偏向锁,有必要才会逐步。

锁升级路径

巨人的肩膀

  • https://kaiwu.lagou.com/course/courseInfo.htm?courseId=16#/detail/pc?id=265
  • https://blog.csdn.net/hollis_chuang/article/details/80922794

-END-

如果看到这里,喜欢这篇文章的话,请帮点个好看。微信搜索「一个优秀的废人」,关注后回复「 1024」送你一套完整的 java 教程(包括视频)。回复「 电子书」送你全编程领域电子书 (不只Java)。

代码语言:javascript
复制
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-01-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一个优秀的废人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 自适应的自旋锁
  • 锁消除
  • 锁粗化
  • 偏向锁 / 轻量级锁 / 重量级锁
  • 锁升级的路径
  • 巨人的肩膀
相关产品与服务
应用性能监控
应用性能监控(Application Performance Management,APM)是一款应用性能管理平台,基于实时多语言应用探针全量采集技术,为您提供分布式性能分析和故障自检能力。APM 协助您在复杂的业务系统里快速定位性能问题,降低 MTTR(平均故障恢复时间),实时了解并追踪应用性能,提升用户体验。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档