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

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

(0)

相关推荐

  • Java 8跳过本次循环,继续执行以及跳出循环,终止循环的代码实例

    在Java8之前,最开始使用for i 循环,很老旧,后来有了高级的for each 循环,然后这个跳出本次循环和跳出所有的for循环,都简单,稍微没见过的就是跳出多层for循环. 然后就是Java8出的foreach循环,这个循环里面,break和continue都不管用啦.需要使用return,这个只能跳过本次循环,还是会继续执行for循环的,那么怎么跳出这个Java8的foreach循环呢? 下面对所有的循环,都来了一次操作. 看看如何,跳出当前循环,继续执行:或者直接跳出for循环:或者

  • Java中增强for循环的实现原理和坑详解

    前言 引入增强for循环的原因:在JDK5以前的版本中,遍历数组或集合中的元素,需要先获得数组的长度或集合的迭代器,比较麻烦. JDK5中定义了一种新的语法----增强for循环,以简化此类操作.增强for循环只能用在数组或实现Iterable接口的集合上. 语法格式: for(变量类型 变量:需迭代的数组或集合){ } 在JAVA中,遍历集合和数组一般有以下三种形式: for (int i = 0; i < list.size(); i++) { System.out.print(list.g

  • Java编程几个循环实例代码分享

    有关Java循环的内容,编程中还是比较常用的,下面分享给大家几个循环的示例代码,练习一下. 1.循环输出1到100之间所有能被3或能被4整除的数. package com.hz.loop02; /** * 1.循环输出1到100之间所有能被3或能被4整除的数. * @author ztw * */ public class Practice01 { public static void main(String[] args) { for (int i=1;i<=100;i++){ //判断下是否

  • 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; in

  • Java中break、continue、return在for循环中的使用

    引言:在使用循环的时候,循环里面带有break.continue.return的时候经常弄混,今天特意整理了下,以待后用... for (int i = 1; i < 5; i++) { System.out.println("i==for=>"+i); while(i%2==0){ System.out.println("i==while==>"+i); break;//终止while循环,继续for后面的代码;(终止当前(while)循环,继续

  • Java使用for循环解决经典的鸡兔同笼问题示例

    本文实例讲述了Java使用for循环解决经典的鸡兔同笼问题.分享给大家供大家参考,具体如下: for循环经典,鸡兔同笼问题 问题:鸡兔同笼,鸡兔一共35只.笼子里脚一共94只,请问分别有多少只鸡和兔? 思路:首先明确思路,鸡的数量*2加上兔子的数量*4等于脚的总数94,这是一个关键点, 代码很简单,但是关键的条件却要花很多时间去找,要是不明白的真的是很烦啊. 利用for循环列举出所有可能直到if满足条件, 列出表达式 鸡*2 加 兔*4 等于 脚总数94 ,这是if的判断条件,满足就可以直接输出

  • Java 锁粗化与循环问题

    1. 写在前面 "JVM 解剖公园"是一个持续更新的系列迷你博客,阅读每篇文章一般需要5到10分钟.限于篇幅,仅对某个主题按照问题.测试.基准程序.观察结果深入讲解.因此,这里的数据和讨论可以当轶事看,并没有做一致性.写作风格.句法和语义错误.重复或一致性检查.如果选择采信文中内容,风险自负. Aleksey Shipilёv,JVM 性能极客 推特 @shipilev 问题.评论.建议发送到 aleksey@shipilev.net 译注:锁粗化(Lock Coarsening).锁

  • Java锁擦除与锁粗化概念和使用详解

    目录 一.什么是锁擦除 二.锁擦除的演示 三.什么是锁粗化 四.锁粗化的演示 一.什么是锁擦除 锁擦除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行擦除.锁擦除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行. 二.锁擦除的演示 public class LockErasureDemo { publi

  • 浅谈Java锁的膨胀过程以及一致性哈希对锁膨胀的影响

    目录 1.锁优化 1.1.锁消除 1.2.锁粗化 1.3.自旋锁 1.4.自适应自旋锁 1.5.锁膨胀 2.锁膨胀实战 2.1.jol工具 2.2.锁膨胀测试代码 2.3.输出分析 2.4.锁释放 3.一致性哈希对锁膨胀的影响 4.锁性能测试 1.锁优化 在JDK6之前,通过synchronized来实现同步效率是很低的,被synchronized包裹的代码块经过javac编译后,会在代码块前后加上monitorenter和monitorexit字节码指令,被synchronized修饰的方法则

  • 浅谈Java锁机制

    目录 1.悲观锁和乐观锁 2.悲观锁应用 3.乐观锁应用 4.CAS 5.手写一个自旋锁 1.悲观锁和乐观锁 我们可以将锁大体分为两类: 悲观锁 乐观锁 顾名思义,悲观锁总是假设最坏的情况,每次获取数据的时候都认为别的线程会修改,所以每次在拿数据的时候都会上锁,这样其它线程想要修改这个数据的时候都会被阻塞直到获取锁.比如MySQL数据库中的表锁.行锁.读锁.写锁等,Java中的synchronized和ReentrantLock等. 而乐观锁总是假设最好的情况,每次获取数据的时候都认为别的线程不

  • 关于Java锁性能提高(锁升级)机制的总结

    目录 Java锁性能提高机制 锁偏向 轻量级锁 自旋锁 重量级锁 Java锁升级简述 对象头结构 synchronized关键字 monitor 锁的四种状态 Java锁性能提高机制 锁的使用很难避免,如何尽量提高锁的性能就显得比较重要了 锁偏向 所谓的偏向锁是指在对象实例的Mark Word(说白了就是对象内存中的开头几个字节保留的信息,如果把一个对象序列化后明显可以看见开头的这些信息),为了在线程竞争不激烈的情况下,减少加锁及解锁的性能损耗(轻量级锁涉及多次CAS操作)在Mark Word中

  • java锁机制ReentrantLock源码实例分析

    目录 一:简述 二:ReentrantLock类图 三:流程简图 四:源码分析 lock()源码分析: 非公平实现: 公平锁实现: tryAcquire()方法 公平锁实现: 非公平锁实现: addWaiter() acquireQueued() shouldParkAfterFailedAcquire() parkAndCheckInterrupt() unlock()方法源码分析: tryRelease() unparkSuccessor() 五:总结 一:简述 ReentrantLock是

  • Java用数组实现循环队列的示例

    复习了下数据结构,用Java的数组实现一下循环队列. 队列的类 //循环队列 class CirQueue{ private int QueueSize; private int front; private int rear; private int[] queueList ; public CirQueue(int QueueSize){ this.QueueSize = QueueSize; queueList = new int[QueueSize]; front = 0; rear =

  • Java中增强for循环在一维数组和二维数组中的使用方法

    一维数组: int[] a={1,2,3}; for(int i:a) { System.out.print(i+" "); } 输出:1 2 3 二维数组: import java.util.Scanner; public class tet { public static void main(String[] args) { //int[][] b={{1,2,3},{4,5,6}};行 int[][] a=new int[5][];//必须明确行数 for(int i=0;i&l

  • 详解JAVA中的for-each循环与迭代

    在学习java中的collection时注意到,collection层次的根接口Collection实现了Iterable<T>接口(位于java.lang包中),实现这个接口允许对象成为 "foreach" 语句的目标,而此接口中的唯一方法,实现的就是返回一个在一组 T 类型的元素上进行迭代的迭代器. 一.迭代器Iterator 接口:Iterator<T> public interface Iterator<E>{ boolean hasNext

  • 浅谈java 增强型的for循环 for each

    For-Each循环 For-Each循环也叫增强型的for循环,或者叫foreach循环. For-Each循环是JDK5.0的新特性(其他新特性比如泛型.自动装箱等). For-Each循环的加入简化了集合的遍历. 其语法如下: for(type element: array) { System.out.println(element); } 例子 其基本使用可以直接看代码: 代码中首先对比了两种for循环:之后实现了用增强for循环遍历二维数组:最后采用三种方式遍历了一个List集合. i

随机推荐