`
qqdwll
  • 浏览: 131216 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

Java6中线程优化及基准测试思路 (二)

阅读更多
转载: http://www.infoq.com/cn/articles/java-threading-optimizations-p2

在本文的第一部分中,我们通过一个单一线程的基准,比较了同步的StringBuffer和非同步的StringBuilder之间的性能。从最初的基准测试结果来看,偏向锁提供了最佳的性能,比其他的优化方式更有效。测试的结果似乎表明获取锁是一项昂贵的操作。但是在得出最终的结论之前,我决定先对结果进行检验:我请我的同事们在他们的机器上运行了这个测试。尽管大多数结果都证实了我的测试结果,但是有一些结果却完全不同。在本文的第二部分中,我们将更深入地看一看用于检验测试结果的技术。最后我们将回答现实中的问题:为什么在不同的处理器上的锁开销差异如此巨大?


基准测试中的陷阱
通过一个基准测试,尤其是一个“小规模基准测试”(microbenchmark),来回答这个问题是非常困难的。多半情况下,基准测试会出现一些与你期望测量的完全不同的情景。即使当你要测量影响这个问题的因素时,结果也会被其他的因素所影响。有一点在这个实验开始之初就已经很明确了,即这个基准测试需要由其他人全面地进行审查,这样我才能避免落入报告无效基准测试数据的陷阱中。除了其他人的检查以外,我还使用了一些工具和技术来校验结果,这些我会在下面的几节中谈到。

结果的统计处理
大多数计算机所执行的操作都会在某一固定的时间内完成。就我的经验而言,我发现即使是那些不确定性的操作,在大多数条件下基本上也能在固定的时间内完成。正是根据计算的这种特性,我们可以使用一种工具,它通过测量让我们了解事情何时开始变得不正常了。这样的工具是基于统计的,其测量结果会有些出入。这就是说,即使看到了一些超过正常水平的报告值,我也不会做过多过的解释的。原因是这样的,如果我提供了指令数固定的CPU,而它并没有在相对固定的时间内完成的话,就说明我的测量受到了一些外部因素的影响。如果测试结果出现了很大的异常,则意味着我必须找到这个外部的影响进而解决它。

尽管这些异常效果会在小规模基准测试中被放大,但它不至于会影响大规模的基准测试。对于大规模的基准测试来说,被测量的目标应用程序的各个方面会彼此产生干扰,这会带来一些异常。但是异常仍然能够提供一些很有益的信息,可以帮助我们对干扰级别作出判断。在稳定的负荷下,我并不会对个别异常情况感到意外;当然,异常情况不能过多。对于那些比通常结果大一些或小一些的结果,我会观察测试的运行情况,并将它视为一种信号:我的基准测试尚未恰当地隔离或者设置好。这样对相同的测试进行不同的处理,恰恰说明了全面的基准测试与小规模基准测试之间的不同。

最后一点,到此为止仍然不能说明你所测试的就是你所想的。这至多只能说明,对于最终的问题,这个测试是最有可能是正确的。

预热方法的缓存
JIT会编译你的代码,这也是众多影响基准测试的行为之一。Hotspot会频繁地检查你的程序,寻找可以应用某些优化的机会。当找到机会后,它会要求 JIT编译器重新编译问题中的某段代码。此时它会应用一项技术,即当前栈替换(On Stack Replacement,OSR),从而切换到新代码的执行上。执行OSR时会对测试产生各种连锁影响,包括要暂停线程的执行。当然,所有这样的活动都会干扰到我们的基准测试。这类干扰会使测试出现偏差。我们手头上有两款工具,可以帮助我们标明代码何时受到JIT的影响了。第一个当然是测试中出现的差异,第二个是-XX:-PrintCompilation标记。幸运的是,如果不是所有的代码在测试的早期就进行JIT化处理,那么我们可以将它视为另外一种启动时的异常现象。我们需要做的就是在开始测量前,先不断地运行基准测试,直到所有代码都已经完成了JIT化。这个预热的阶段通常被称为“预热方法的缓存 ”。

大多数JVM会同时运行在解释的与本机的模式中。这就是所谓的混合模式执行。随着时间的流逝,Hotspot和JIT会根据收集的信息将解释型代码转化为本机代码。Hotspot为了决定应该使用哪种优化方案,它会抽样一些调用和分支。一旦某个方法达到了特定的阈值后,它会通知JIT生成本机代码。这个阈值可以通过-XX:CompileThreshold标记来设定。例如,设定-XX:CompileThreshold=10000,Hotspot会在代码被执行10,000次后将它编译为本机代码。


堆管理
下一个需要考虑的是垃圾收集,或者更广为人知的名字—堆管理。在任何应用程序执行的过程中,都会定期地发生很多种内存管理活动。它们包括:重新划分栈空间大小、回收不再被使用的内存、将数据从一处移到另一处等等。所有这些行为都导致JVM影响你的应用程序。我们面对的问题是:基准测试中是否需要将内存维护或者垃圾回收的时间包括进来?问题的答案取决于你要解决的问题的种类。在本例中,我只对获取锁的开销感兴趣,也就是说,我必须确保测试中不能包含垃圾回收的时间。这一次,我们又能够通过异常的现象来发现影响测试的因素,一旦出现这种问题,垃圾回收都是一个可能的怀疑对象。明确问题的最佳方式是使用 -verbose:gc标志,开启GC的日志功能。

在这个基准测试中,我做了大量的String、StringBuffer和StringBuilder操作。在每次运行的过程中大概会创建4千万个对象。对于这样一种数量级的对象群来说,垃圾回收毫无疑问会成为一个问题。我使用两项技术来避免。第一,提高堆空间的大小,防止在一个迭代中出现垃圾回收。为此,我利用了如下的命令行:
java -server -XX:+EliminateLocks -XX:+UseBiasedLocking -verbose:gc -XX:NewSize=1500m   -XX:SurvivorRatio=200000 LockTest


然后,加入清单1的代码,它为下一次迭代准备好堆空间。
System.gc();
Thread.sleep(1000);

清单1. 运行GC,然后进行短暂的休眠。

休眠的目的在于给垃圾回收器充分的时间,在释放其他线程之后完成工作。有一点需要注意:如果没有CPU任何活动,某些处理器会降低时钟频率。因此,尽管CPU时钟会自旋等待,但引入睡眠的同时也会引入延迟。如果你的处理器支持这种特性,你可能必须要深入到硬件并且关闭掉“节能”功能才行。

前面使用的标签并不能阻止GC的运行。它只表示在每一次测试用例中只运行一次GC。这一次的暂停非常小,它产生的开销对最终结果的影响微乎其微。对于我们这个测试来说,这已经足够好了

偏向锁延迟
还有另外一种因素会对测试结果产生重要的影响。尽管大多数优化都会在测试的早期发生,但是由于某些未知的原因,偏向锁只发生在测试开始后的三到四秒之后。我们又要重述一遍,异常行为再一次成为判断是否存在问题的重要标准了。-XX:+TraceBiasedLocking标志可以帮助我们追踪这个问题。还可以延长预热时间来克服偏向锁导致的延迟。
Hotspot提供的其他优化

Hotspot不会在完成一次优化后就停止对代码的改动。相反,它会不断地寻找更多的机会,提供进一步的优化。对于锁来说,由于很多优化行为违反了 Java存储模型中描述的规范,所以它们是被禁止的。然而,如果锁已经被JIT化了,那么这些限制很快就会消失。在这个单线程化的基准测试中,Hotspot可以非常安全地将锁省略掉。这样就会为其他的优化行为打开大门;比如方法内联、提取循环不变式以及死代码的清除。

如果仔细思考下面的代码,可以发现A和B都是不变的,我们应该把它抽取出来放到循环外面,并引入第三个变量,这样可以避免重复的计算,正如清单3中所示的那样。通常,这都是程序员的事情。但是Hotspot 可以识别出循环不变式并把它们抽取到循环体外面。因此,我们可以把代码写得像清单2那样,但是它执行时其实更类似于清单3的样子。  int A = 1;  int B = 2;  int sum = 0;  for (int i =  0; i < someThing; i++) sum += A + B;清单2 循环中包含不变式  int A = 1;  int B = 2;  int sum = 0;  int invariant = A + B;  for (int i =  0; i < someThing; i++) sum += invariant; 清单3 不变式已抽取到循环之外


这些优化真的应该允许么?还是我们应该做一些事情防止它的发生?这个有待商榷。但至少,我们应该知道是否应用了这些优化。我们绝对要避免“死代码消除”这种优化的出现,否则它会彻底扰乱我们的测试!Hotspot能够识别出我们没有使用concatBuffer和concatBuilder操作的结果。或者可以说,这些操作没有边界效应。因此没有任何理由执行这些代码。一旦代码被标识为已“死亡”,JIT就会除去它。好在我的基准测试迷惑了 Hotspot,因此它并没有识别出这种优化,至少目前还没有。

如果由于锁的存在而抑制了内联,反之没有锁就可能出现内联,那么我们要确保在测试结果中没有包含额外的方法调用。现在可以用到的一种技术是引入一个接口(清单4)来迷惑Hotspot。

public interfaceConcat {
 	String concatBuffer(String s1, String s2, String s3);
 	String concatBuilder(String s1, String s2, String s3);

public class LockTest implements Concat {

...}

清单4 使用接口防止方法内联

防止内联的另一种方法是使用命令行选项-XX:-Inline。我已经验证,方法内联并没有给基准测试的报告带来任何不同

执行栈输出
最后,请看下面的输出结果,它使用了下面的命令行标识。
>java -server -XX:+DoEscapeAnalysis -XX:+PrintCompilation -XX:+EliminateLocks -XX:+UseBiasedLocking -XX:+TraceBiasedLocking LockTest



图1 基准测试的执行栈输出

JVM默认会启动12个线程,包括:主线程、对象引用处理器、Finalize、Attach监听器等等。上图中第一个灰色段显示的是这些线程的对齐,它们可以使用偏向锁(注意所有地址都以00结尾)。你尽管忽略可以忽略它们。接下来的黄色段包含了已编译方法的信息。我们看一下第5行和12行,能够发现它们都标记了一个额外的“s”。表1的信息告诉我们这些方法都是同步的。包含了“%”的各行已经使用了OSR。红色的行是偏向锁被激活的地方。最底下的蓝绿色框是基准测试开始计时的地方。从记录基准测试开始时间的输出中可以看到,所有编译都已经发生了。这说明前期的预热阶段足够长了。如果你想了解日志输出规范的更多细节,可以参考这个页面http://forum.java.sun.com/thread.jspa?forumID=27&messageID=980887&threadID=235212和这篇文章http://www.unixville.com/~moazam/stories/2004/06/17/thePrintcompilationFlagAndHowToReadItsOutput.html



表1 编译示例码


单核系统下的结果
尽管我的多数同事都在使用Intel Core 2 Duo处理器,但还是有一小部分人使用陈旧的单核机器。在这些陈旧的机器上,StringBuffer基准测试的结果和StringBuilder实现的结果几乎相同。由于产生这种不同可能是多种因素使然,因此我需要另外一个测试,尝试忽略尽可能多的可能性。最好的选择是,在BIOS中关闭Core 2 Duo中的一个核,然后重新运行基准测试。运行的结果如图2所示。


图2 单核系统的性能

在多核环境下运行的时候,关闭了三种优化行为后获得了一个基准值。这次,StringBuilder又保持了平稳的吞吐量。更有趣的是,尽管 StringBuffer比StringBuilder要稍慢,但是在多核平台下,StringBuffer的性能更接近于StringBuilder。从这个测试开始我们将一步步勾勒出基准测试的真实面目。

在多核的世界中,线程间共享数据的现实呈现出一种全新的面貌。所有现代的CPU必须使用本地存储的缓存,将获取指令和数据的延迟降到最低。当我们使用锁的时候,会导致一次存储关卡(Barrier)被插入到执行路径中。存储关卡像一个信号,它通知CPU此时必须和其他所有的CPU进行协调,以此获得最新的数值。为了完成这个任务,CPU之间将要彼此通讯,从而导致每个处理器暂定当前正在运行的应用程序线程。这个过程要花多少时间已经成了CPU存储模型的指标之一。越是保守的存储模型,越是线程安全的,但是它们在协调各个处理器核的时候也要花费更多的时间。在Core 2 Duo上,第二个核将固定的运行基准从3731ms提高到了6574ms,或者说增加了176%。很明显,Hotspot所提供的任何帮助都能明显改进我们的应用程序的总体性能。

逸出分析真的起作用了么?
现在,还有一种优化很明显会起作用,但是我们还没有考虑,它就是锁省略。锁省略是最近才实现的技术,而且它依赖于逸出分析,后者是一种Profiling 技术,其自身也是刚刚才实现的。为了稳妥一些,各公司和组织都宣称这些技术只有在有限的几种情况下才起作用。比如,在一个简单的循环里,对一个局部变量执行递增,且该操作被包含在一个同步块内,由一个局部的锁保护着。这种情况下逸出分析是起作用的http://blog.nirav.name/2007_02_01_archive.html。同时它在Mont Carlo的Scimark2基准测试中可以工作(参见http://math.nist.gov/scimark2/index.html)。

将逸出分析包含在测试中
那么,为什么逸出分析可以用于上述的情况中,却不能用于我们的基准测试中?我曾经尝试过将StringBuffer和 StringBuilder的部分方法进行内联。我也修改过代码,希望可以强制逸出分析运行。我想看到锁最终被忽略,而性能可以获得大幅提升。老实说,处理这个基准测试的过程既困惑,又让人倍感挫折。我必须无数次地在编辑器中使用ctrl-z,以便恢复到前面一个我认为逸出分析应该起作用的版本,但是却不知由于什么原因,逸出分析却突然不起作用了。有时,锁省略却又会莫名其妙地出现。

最后,我认识到激活锁省略似乎和被锁对象的数据大小有关系。你运行清单2的代码就会看到这一点。正如你所看到的,无论运行多少次,结果都毫无区别,这说明DoEscapeAnalysi没有产生影响。
>java -server -XX:-DoEscapeAnalysis EATest thread unsafe: 941 ms. thread safe: 1960 ms. Thread safety overhead: 208% >java -server -XX:+DoEscapeAnalysis EATest thread unsafe: 941 ms. thread safe: 1966 ms. Thread safety overhead: 208%


在下面的两次运行中,我移除了ThreadSafeObject类中一个没有被用过的域。如你所见,当开启了逸出分析,所有性能有了很大的提高。
>java -server -XX:-DoEscapeAnalysis EATest thread unsafe: 934 ms. thread safe: 1962 ms. Thread safety overhead: 210% >java -server -XX:+DoEscapeAnalysis EATest thread unsafe: 933 ms. thread safe: 1119 ms. Thread safety overhead: 119%


逸出分析的数目在Windows和Linux上都能看到。然而在Mac OS X上,即使有额外未被使用的变量也不会有任何影响,任何版本的基准测试的结果都是120%。这让我不由地相信在Mac OS X上有效性的范围比其他系统更广泛。我猜测这是由于它的实现比较保守,根据不同条件(比如锁对象数据大小和其他OS特定的特性)及早地关掉了它。

结论
当我刚开始这个实验,解释应用各种锁优化的Hotspot的有效性的时候,我估计它将花费我几个小时的时间,最终这会丰富我的blog的内容。但是就像其他的基准测试一样,对结果进行验证和解释的过程最终耗费了几周的时间。同样,我也与很多专家进行合作,他们分别花费了大量时间检查结果,并发表他们的见解。即使在这些工作完成以后,仍然很难说哪些优化起作用了,而哪些没有起作用。尽管这篇文章引述了一组测试结果,但它们是特定我的硬件和系统的。大家可以考虑是否能在自己的系统上看到相同类型的测试结果。另外,我最初认为这不过是个小规模基准测试,但是后来它逐渐既要满足我,也要满足所有审核代码的人,而且去掉了Hotspot不必要的优化。总之,这个实验的复杂度远远地超出了我的预期。

如果你需要在多核机器上运行多线程的应用程序,并且关心性能,那么很明显,你需要不断地更新所使用的JDK到最新版本。很多(但不是全部)前面的版本的优化都可以在最新的版本中获得兼容。你必须保证所有的线程优化都是激活的。在JDK 6.0中,它们默认是激活的。但是在JDK 5.0中,你需要在命令行中显式地设置它们。如果你在多核机器上运行单线程的应用程序,就要禁用除第一个核以外所有核的优化,这样会使应用程序运行得更快。

在更低级的层面上,单核系统上锁的开销远远低于双核处理器。不同核之间的协调,比如存储关卡语义,通过关掉一个核运行的测试结果看,很明显会带来系统开销。我们的确需要线程优化,以此降低这一开销。幸运的是,锁粗化和(尤其是)偏向锁对于基准测试的性能确实有明显的影响。我也希望逸出分析与锁省略一起更能够做到更好,产生更多的影响。这项技术会起作用,可只是在很少的情况下。客观地说,逸出分析仍然还处于它的初级阶段,还需要大量的时间才能变得成熟。

最后的结论是,最权威的基准测试是让你的应用程序运行在自己的系统上。当你的多线程应用的性能没有符合你的期望的时候,这篇文章能够为你提供了一些思考问题的启示。而这就是此文最大的价值所在。

关于Jeroen Borgers
Jeroen Borger是Xebia 的资深咨询师。Xebia是一家国际IT咨询与项目组织公司,专注于企业级Java和敏捷开发。Jeroen帮助他的客户攻克企业级Java系统的性能问题,他同时还是Java性能调试课程的讲师。他在从1996年开始就可以在不同的Java项目中工作,担任过开发者、架构师、团队lead、质量负责人、顾问、审核员、性能测试和调试员。他从2005年开始专注于性能问题。
  • 大小: 130.9 KB
  • 大小: 53.2 KB
  • 大小: 20.7 KB
分享到:
评论

相关推荐

    Java 模拟线程并发

    Java 模拟线程并发 Java, 模拟线程并发,线程,并发 Java, 模拟线程并发,线程,并发 Java, 模拟线程并发,线程,并发 Java, 模拟线程并发,线程,并发

    Java中懒汉单例设计模式线程安全测试

    Java中懒汉单例设计模式线程安全测试,单例设计模式的测试

    Java线程Java线程Java线程Java线程

    Java线程Java线程Java线程Java线程Java线程Java线程

    java多线程笔记

    二、Java中的线程 3 三、Java中关于线程的名词解释 3 四、线程的状态转换和生命周期 4 Java线程:创建与启动 7 Java线程:线程名称的设定及获取 10 Java线程:线程栈模型与线程的变量 12 Java线程:线程的调度-休眠 ...

    线程 JAVA java线程 java线程第3版 java线程第2版第3版合集

    Java的线程工具易于使用,并且像Java中的其他东西一样可以在不同的平台之间移植。这是一件好事,因为如果没有线程,那么除了最简单的applet之外,几乎不可能编写出任何程序。如果你想使用Java,就必须学习线程。 ...

    java多线程编程总结

    Java线程:概念与原理 Java线程:创建与启动 Java线程:线程栈模型与线程的变量 Java线程:线程状态的转换 Java线程:线程的同步与锁 Java线程:线程的交互 Java线程:线程的调度-休眠 Java线程:线程的调度-优先级 ...

    Java的线程和Java AppletJava的线程和Java AppletJava的线程和Java Applet

    Java的线程和Java AppletJava的线程和Java AppletJava的线程和Java AppletJava的线程和Java AppletJava的线程和Java Applet

    Java线程讲解Java线程讲解

    Java线程讲解Java线程讲解Java线程讲解Java线程讲解Java线程讲解Java线程讲解Java线程讲解Java线程讲解Java线程讲解Java线程讲解

    Java多线程编程总结

    Java 线程系列博文总结word化,编目如下,欢迎互相学习交流: Java线程:概念与原理 Java线程:创建与启动 Java线程:线程栈模型与线程的变量 Java线程:线程状态的转换 Java线程:线程的同步与锁 Java线程:...

    论文研究-Java多线程测试策略及测试方法探讨.pdf

    在分析Java 多线程特性的基础上, 探讨了Java 多线程的测试策略及测试方法, 提出Java 多线程测试由类测试、集成模块测试和系统测试三个层次组成, 并讨论了多线程的继承测试、同步测试以及效率测试。

    Java多线程机制(讲述java里面与多线程有关的函数)

    9.1 Java中的线程 9.2 Thread的子类创建线程 9.3 使用Runable接口 9.4 线程的常用方法 9.5 GUI线程 9.6 线程同步 9.7 在同步方法中使用wait()、notify 和notifyAll()方法 9.8 挂起、恢复和终止线程 9.9 计时器线程...

    java 线程java 线程

    java 线程java 线程java 线程java 线程java 线程java 线程java 线程java 线程java 线程

    Java线程详解大全

    Java线程Java线程Java线程Java线程Java线程Java线程Java线程Java线程Java线程Java线程Java线程Java线程Java线程Java线程Java线程

    JAVA单线程多线程

    单线程 单线程 单线程 单线程 单线程 单线程

    Java线程高清晰中文第二版

    Java的线程工具易于使用,并且像Java中的其他东西一样可以在不同的平台之间移植。这是一件好事,因为如果没有线程,那么除了最简单的applet之外,几乎不可能编写出任何程序。如果你想使用Java,就必须学习线程。

    java线程安全测试

    java 线程安全的几个测试小例子,充分的理解JMM中的线程内存模型

    Java多线程设计模式上传文件

    Java多线程设计模式上传文件Java多线程设计模式上传文件Java多线程设计模式上传文件Java多线程设计模式上传文件Java多线程设计模式上传文件Java多线程设计模式上传文件Java多线程设计模式上传文件Java多线程设计模式...

    java多线程读取文件

    Java多线程读大文件 java多线程写文件:多线程往队列中写入数据

    java多线程导出excel(千万级别)优化

    轻松解决普通poi形式导出Excel的中出现的栈溢出问题,此资源可实现千万级数据分批导出csv文件,csv大数据量导出(千万级别,不会内存溢出),多线程导出 ,生产环境已经很稳定的使用着

    java多线程ppt

    java多线程PPT 多线程基本概念 创建线程的方式 线程的挂起与唤醒 多线程问题

Global site tag (gtag.js) - Google Analytics