标签分类
技术文章
当前位置:主页 > 计算机编程 > java > Java锁粗化与循环问题

Java锁粗化与循环问题详解

  • 发布时间:
  • 作者:码农之家原创
  • 点击:172

Java锁粗化与循环问题

这篇文章主要知识点是关于java,锁粗化,java循环,Java锁粗化与循环问题,的内容,如果大家想对相关知识点有系统深入的学习,可以参阅以下电子书

实战Java高并发程序设计
  • 类型:Java大小:104.1 MB格式:PDF出版:电子工业出版社作者:葛一鸣
立即下载

更多相关的学习资源可以参阅 程序设计电子书Java电子书、等栏目。

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;
      }
    }
  }
}

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

这里有一些重要的技巧:

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

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

运行环境 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 锁粗化与循环问题,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对码农之家网站的支持!

如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

以上就是本次给大家分享的全部知识点内容总结,大家还可以在下方相关文章里找到儿童python编程入门书籍推、 解决axios.interceptors.respon、 详解vue axios封装请求状态、 等java文章进一步学习,感谢大家的阅读和支持。

上一篇:《Java编程的逻辑》读书笔记、评价

下一篇:Hibernate中使用HQLQuery查询数据的方法实例

展开 +

收起 -

学习笔记
网友NO.440383

详谈Java枚举、静态导入、自动拆装箱、增强for循环、可变参数

一、枚举简介 1、什么是枚举? 需要在一定范围内取值,这个值只能是这个范围内中的任意一个 现实场景:交通信号灯,有三种颜色,但是每次只能亮三种颜色里面的任意一个 2、使用一个关键字 enum enum Color3 { RED,GREEN,YELLOW; } *枚举的构造方法也是私有化的 *特殊枚举的操作 **在枚举类里面有构造方法 **在构造方法里面有参数,需要在每个实例上都写参数 **在枚举类里面有抽象方法 **在枚举的每个实例里面都重写这个抽象方法 3、枚举的api的操作 **name():返回枚举的名称 **ordinal():枚举的下标 **valueof(ClassT enumType,String name):得到枚举的对象 **还有两个方法,不在api文档里,编译自动生成 ***valueof(String name) 转换枚举对象 ***values() 获得所有枚举对象数组 //知道枚举的对象,得到枚举名称和下标 @Test public void test1() { //得到枚举对象 Color100 c100 = Color100.RED; //得到枚举名称 String name = c100.name(); //得到枚举的下标 int idx = c100.ordinal(); System.out.println(name+":"+idx); } //知道枚举的名称,得到枚举的对象和下标 @Test public void test2() { String name1 = "GREEN"; //得到对象 Color100 c1 = Color100.valueOf(name1); //得到枚举下标 int idx1 = c1.ordinal(); System.out.println(idx1); } //知道枚举的下标,得到枚举的对象和名称 @Test public void test3() { int idx2 = 2; //得到枚举对象 C……

网友NO.425003

通过循环优化 JavaScript 程序

前言 对于提高 JavaScript 程序的性能这个问题,最简单同时也是很容易被忽视的方法就是学习如何正确编写高性能循环语句。本文将会帮你解决这个问题。 我们将看到 JavaScript 中主要的循环类型,以及如何针对它们进行高效编码。 现在开始! 循环性能 谈到循环性能,争论的焦点始终会集中到关于应该使用哪种循环,哪个是速度最快、性能最好的?事实上,在 JavaScript 提供的四种循环类型中,只有一种比其他循环慢得多 ——  for-in 循环。 对循环类型的选择应基于你的需求而不是性能问题。 有两个主要因素有助于改善循环性能 —— 每次迭代完成的工作和迭代次数。 在下面的内容中,我们将会看到通过对这两点的优化,可以对循环的整体性能产生积极的影响。 For 循环 在 ECMA-262(定义JavaScript的基本语法和行为的规范)第三版中,定义了四种循环类型。第一个是标准的 for 循环,它与其他类 C 语言的语法相同: for (var i = 0; i 10; i++){//循环体} 这可能是最常用的 JavaScript 循环结构。要了解应该怎样对其进行优化,需要先进行一些分析。 解析 for 循环由四部分组成:初始化,预测试条件,循环体和后执行。它的工作方式如下:首先,执行初始化代码(var i = 0;)。然后是预测试条件(i 10;)。如果预测试条件的计算结果为 true,……

网友NO.782086

JavaScript数据结构之单链表和循环链表

数据结构系列前言: 数据结构作为程序员的基本知识,需要我们每个人牢牢掌握。近期我也展开了对数据结构的二次学习,来弥补当年挖的坑。。。。。。 当时上课的时候也就是跟着听课,没有亲自实现任何一种数据结构,更别提利用数据结构来解决问题了。 现在就来填坑了奋斗 在这里提醒看到我博客的孩子们,如果你还是在校生,永远不要轻视任何一门基础课的学习,这个时候挖的坑,要么需要用双倍的努力去填,要么会直接影响一个人的能力等等。。。。。。 千万别给自己挖坑 进入正题,关于链表的数据结构知识,这里简单介绍下: 链表是一种物理存储单元上非线性、非连续性的数据结构(它在数据逻辑上是线性的),它的每个节点由两个域组成:数据域和指针域。数据域中存储实际数据,指针域则存储着指针信息,指向链表中的下一个元素或者上一个元素。正是由于指针的存在,链表的存储在物理单元是非连续性的。 链表的优点和缺点同样明显。和线性表相比,链表在添加和删除节点上的效率更高,因为其只需要修改指针信息即可完成操作,而不像线性表(数组)那样需要移动元素。同样的,链表的长度在理论上也是无限的(在存储器容量范围内),并可以动态变化长度,相比线性表优势很大。 相应的,由……

网友NO.243234

Java三种循环求和方法

注意:100之和为5050 普通for循环: public class HundredSum { public static void main(String[] args){ int x=0; for(int i=1;i=100;i++){ x=x+i;//x+=i; } System.out.print(x); } } while循环: public class HundredSum { public static void main(String[] args){ int x=0; int i; while (i=100){ x=x+i; //x+=i; i++; } System.out.print(x); } } do-while循环: public class HundredSum{ public static void main(String[] args){ int i=0,x=0; do{ x=x+i; //x+=i; i++; }while (i=100); //先循环do语句块,再执行while,不满足while条件则跳出循环 System.out.print(x); } } 以上就是本次整理的3种常用的求和方法,感谢大家对码农之家的支持。 ……

<
1
>

Copyright 2018-2019 xz577.com 码农之家

版权责任说明