优秀的编程知识分享平台

网站首页 > 技术文章 正文

Java 锁粗化与循环(java锁的优化)

nanyue 2024-10-07 11:25:06 技术文章 14 ℃

1. 写在前面

“JVM 解剖公园”是一个持续更新的系列迷你博客,阅读每篇文章一般需要5到10分钟。限于篇幅,仅对某个主题按照问题、测试、基准程序、观察结果深入讲解。因此,这里的数据和讨论可以当轶事看,并没有做一致性、写作风格、句法和语义错误、重复或一致性检查。如果选择采信文中内容,风险自负。

Aleksey Shipilёv,JVM 性能极客

推特 @shipilev

问题、评论、建议发送到 aleksey@shipilev.net

译注:锁粗化(Lock Coarsening)。锁粗化是合并使用相同锁对象的相邻同步块的过程。如果编译器不能使用锁省略(Lock Elision)消除锁,那么可以使用锁粗化来减少开销。


2. 问题

众所周知,Hotspot 确实进行了锁粗化优化,可以有效合并几个相邻同步块,从而降低锁开销。能够把下面的代码

synchronized (obj) {
 // 语句 1
}
synchronized (obj) {
 // 语句 2
}


转化为

synchronized (obj) {
 // 语句 1
 // 语句 2
}


问题来了,Hotspot 能否对循环进行这种优化?例如,把

for (...) {
 synchronized (obj) {
 // 一些操作
 }
}


优化成下面这样?

synchronized (this) {
 for (...) {
 // 一些操作
 }
}


理论上,没有什么能阻止我们这样做,甚至可以把这种优化看作只针对锁的优化,像 loop unswitching 一样。然而,缺点是可能把锁优化后变得过粗,线程在执行循环时会占据所有的锁。

译注:Loop unswitching 是一种编译器优化技术。通过复制循环主体,在 if 和 else 语句中放一份循环体代码,实现将条件句的内部循环移到循环外部,进而提高循环的并行性。由于处理器可以快速运算矢量,因此执行速度得到提升。


3. 实验

要回答这个问题,最简单的办法就是找到 Hotspot 优化的证据。幸运的是,有了 JMH 帮助这项工作变得非常简单。JMH 不仅在构建基准测试时有用,并且在分析基准测试方面同样好用。让我们从一个简单的基准测试开始:

@Fork(..., jvmArgsPrepend = {"-XX:-UseBiasedLocking"})
@State(Scope.Benchmark)
public class LockRoach {
 int x;
 @Benchmark
 @CompilerControl(CompilerControl.Mode.DONT_INLINE)
 public void test() {
 for (int c = 0; c < 1000; c++) {
 synchronized (this) {
 x += 0x42;
 }
 }
 }
}


(完整的源代码参见这里 ,请查看原文链接)

这里有一些重要的技巧:

  1. 使用 -XX:-UseBiasedLocking 禁用偏向锁(Biased Lock)可以避免启动时间过长。由于偏向锁不会立即启动,在初始化阶段要等待5秒钟(参见 BiasedLockingStartupDelay 选项)
  2. 禁用 @Benchmark 方法内联操作可以帮助我们从反汇编中分离相关内容
  3. 加上“魔数” 0x42 有助于快速从反汇编中定位加法操作


译注:偏向锁(Biased Locking)。尽管 CAS 原子指令相对于重量级锁来说开销比较小,但还是存在非常可观的本地延迟,为了在无锁竞争的情况下避免取锁获过程中执行不必要的 CAS 原子指令提出了偏向锁技术。论文 Quickly Reacquirable Locks ,作者 Dave Dice、Mark Moir、William Scherer III。

欢迎工作一到五年的Java工程师朋友们加入Java程序员开发: 854393687

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!


运行环境 i7 4790K、Linux x86_64、JDK EA 9b156:

Benchmark Mode Cnt Score Error Units
LockRoach.test avgt 5 5331.617 ± 19.051 ns/op


从上面运行数据能分析出什么结果?什么都看不出来,对吧?我们需要调查背后到底发生了什么。这时 -prof perfasm 配置可以派上用场,它能显示生成代码中的热点区域。用默认设置运行,能够发现最热的指令是加锁 lock cmpxchg(CAS),而且只打印指令附近的代码。-prof perfasm:mergeMargin=1000 配置可以将这些热点区域合并保存为输出片段,乍看之下可能觉得有点恐怖。

进一步分析得出连续的跳转指令是锁定或解锁,注意循环次数最多的代码(第一列),可以看到最热的循环像下面这样:

↗ 0x00007f455cc708c1: lea 0x20(%rsp),%rbx
 │ < 省略若干代码,进入 monitor > ; <--- coarsened(粗化)!
 │ 0x00007f455cc70918: mov (%rsp),%r10 ; 加载 $this
 │ 0x00007f455cc7091c: mov 0xc(%r10),%r11d ; 加载 $this.x
 │ 0x00007f455cc70920: mov %r11d,%r10d ; ...hm...
 │ 0x00007f455cc70923: add $0x42,%r10d ; ...hmmm...
 │ 0x00007f455cc70927: mov (%rsp),%r8 ; ...hmmmmm!...
 │ 0x00007f455cc7092b: mov %r10d,0xc(%r8) ; LOL Hotspot,冗余存储,下面省略两行
 │ 0x00007f455cc7092f: add $0x108,%r11d ; 加 0x108 = 0x42 * 4 <-- 展开4次
 │ 0x00007f455cc70936: mov %r11d,0xc(%r8) ; 把 $this.x 回省略若干代码,退出 monitor > ; <--- coarsened(粗化)!
 │ 0x00007f455cc709c6: add $0x4,%ebp ; c += 4 <--- 展开4次
 │ 0x00007f455cc709c9: cmp $0x3e5,%ebp ; c < 1000?
 ╰ 0x00007f455cc709cf: jl 0x00007f455cc708c1


哈哈。循环似乎被展开了4次,然后这4个迭代中实现锁粗化!为了排除循环展开对锁粗化的影响,我们可以通过-XX:LoopUnrollLimit=1 配置裁剪循环展开,再次量化受限后的粗化性能。

译注:Loop unrolling(循环展开),也称 Loop unwinding,是一种循环转换技术。它试图以牺牲二进制大小为代价优化程序的执行速度,这种方法被称为时空折衷。转换可以由程序员手动执行,也可以由编译器优化。


Benchmark Mode Cnt Score Error Units
# Default
LockRoach.test avgt 5 5331.617 ± 19.051 ns/op
# -XX:LoopUnrollLimit=1
LockRoach.test avgt 5 20679.043 ± 3.133 ns/op


哇,性能提升了4倍!显而易见的,因为我们已经观察到最热的指令是加锁 lock cmpxchg。当然,4倍后的粗化锁意味着4倍吞吐量。非常酷,我们是不是可以宣布成功,然后继续前进?还没有。我们必须验证禁用循环展开真正提供了我们想要进行比较的内容。perfasm 的结果似乎表明它含有类似的热点循环,只是跨了一大步。

↗ 0x00007f964d0893d2: lea 0x20(%rsp),%rbx
 │ < 省略若干代码,进入 monitor >
 │ 0x00007f964d089429: mov (%rsp),%r10 ; 加载 $this
 │ 0x00007f964d08942d: addl $0x42,0xc(%r10) ; $this.x += 0x42
 │ < 省略若干代码,退出 monitor >
 │ 0x00007f964d0894be: inc %ebp ; c++
 │ 0x00007f964d0894c0: cmp $0x3e8,%ebp ; c < 1000?
 ╰ 0x00007f964d0894c6: jl 0x00007f964d0893d2 ;


一切都检查 OK。

4. 观察结果

当锁粗化在整个循环中不起作用时,一旦中间看起来好像存在 N 个相邻的加锁解锁操作,另一种循环优化——循环展开会提供常规锁粗化。这将提高性能,并有助于限制粗化的范围,以避免长循环过度粗化。

欢迎工作一到五年的Java工程师朋友们加入Java程序员开发: 854393687

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!


最近发表
标签列表