java 线程优化_Java 6线程优化真的有效吗? -第二部分

java 线程优化

本文的第一部分中,我们使用了单线程基准测试来比较同步StringBuffer和未同步StringBuilder的性能。 最初的基准测试结果表明,与任何其他可用的优化方法相比,偏置锁定提供了最佳的性能提升。 基准测试的结果似乎表明,获取锁是一项昂贵的操作。 但是我没有跳到这个结论,而是决定让我的同事在他们的机器上运行基准测试来验证结果。 尽管大多数结果证实了我的发现,但其中一些结果却完全不同。 在第二部分中,我们将更深入地研究用于验证基准测试结果是否有效的技术。 最后,我们将回答一个真正的问题:为什么在不同的处理器上锁定成本如此不同?

基准陷阱

获得基准,尤其是微基准来回答所提出的问题可能特别困难。 基准测试的结果往往与您要测量的结果完全不同。 即使您碰巧测量了所涉及的效果,这些测量也会被其他效果破坏。 很显然,在本练习开始时,如果我要避免陷入报告不良基准数字的陷阱,就需要对其他基准进行彻底的审查和审查。 除了审核基准测试对象外,我还使用多种工具和技术来验证结果,这些结果将在以下各节中进行讨论。

统计处理结果

计算机将执行的绝大部分操作是固定的时间量。 根据我的经验,我发现即使在大多数情况下,不确定操作也将在几乎恒定的时间内完成。 正是计算的这种特性为我们提供了一种工具,一种度量方法,可以让我们知道事情何时未按预期进行。 该工具是统计数据,度量是方差。 就是说,我真的不想超出“挥手致意”的解释,为什么我们要报告的不仅仅是平均数。 推理是这样的; 如果我为CPU提供了固定数量的指令,但又没有在相对恒定的时间内完成,则表明存在某些因素,一些外部因素会影响我的测量。 当我看到基准数据有很大的差异时,我知道我必须寻找这种外部影响并加以处理。

尽管微基准会夸大这种方差效应,但较大的基准不会。 使用较大的基准,正在研究的应用程序的已测量方面将相互干扰,这将带来一些差异。 但是,方差仍然可以为我们提供一些良好的信息,以衡量干扰的程度。 在恒定负载下,我期望方差为; 好吧,变化不大。 我将查看带有大于和小于大多数差异的方差的运行,并以此作为基准未正确隔离或配置的指示。 对同一度量的这种不同处理实际上指出了完整基准和微基准之间的差异。

最后一点,仍然不是您正在衡量自己认为要衡量的内容的指示。 这仅表明您正在测量的内容,最有可能针对最终问题进行正确的测量。

预热方法缓存

会干扰基准测试的活动之一是代码的JIT编译。 负责的代理商Hotspot一直在对您的应用程序进行性能分析,以寻找机会进行一些优化。 当发现机会时,它将指示JIT编译器重新编译有问题的代码部分。 然后,它使用一种称为“堆栈替换”(OSR)的技术切换到新代码的执行。 执行OSR具有各种连锁React,包括必须暂停执行线程。 当然,所有这些活动都会干扰我们的基准测试。 正是这种干扰会扭曲我们的结果。 我们有两个工具可供使用,以帮助我们确定何时对代码进行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操作。 就所有收费而言,基准每次运行都会创建约4000万个对象。 有了这样的对象搅动水平,毫无疑问垃圾回收将成为一个问题。 我使用了两种技术。 首先,我增加了Eden堆空间大小,以防止在工作台的单次迭代期间运行垃圾回收。 为此,我使用了命令行:

>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运行。 也就是说,每个测试用例只运行一次。 每次暂停时间都足够短,以至于对结果的总体影响很小,这足以满足我们的目的。

偏置锁定延迟

还有另一个影响基准测试结果的影响。 由于大多数优化是在很早就应用的,由于某种未知的原因,偏差锁定仅在运行三到四整秒后才应用。 冒着听起来破纪录的风险,方差再次在确定这是一个问题方面起着重要作用。 标志-XX:+ TraceBiasedLocking有助于解决此问题。 调整了预热时间以解决此延迟。

其他热点提供的优化

应用优化完成后,Hotspot不会停止对代码进行性能分析。 相反,它会继续进行概要分析,以寻找更多机会提供进一步的优化。 带有锁定的情况下,有很多优化是完全不允许的,因为它们会破坏Java内存模型描述的规范。 但是,如果可以将JIT'ed锁删除,则这些限制会很快消失。 对于这种单线程基准测试,对于Hotspot来说,安全地解除锁定是相当安全的。 这将为应用其他优化提供机会,例如; 方法内联,循环不变式提升和无效代码消除。

如果我们在侧边栏中考虑代码,我们可以看到A和B都是不变的,并且可以通过将其提升到循环之外并引入第三个变量来避免重复计算,如第二个清单所示。 传统上,执行此任务是程序员的责任。 但是,Hotspot不仅具有识别循环不变性并将其提升到循环之外的能力。 因此,我们可以在执行类似于清单3中的内容的同时编写清单2中的代码。
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感到困惑,因此它无法识别该优化...。

如果内联在存在锁的情况下被阻止,而在没有锁的情况下被阻止,则我们需要确保在结果中不包括额外的方法调用。 您当前可以用来欺骗Hotspot的一种技术是引入一个接口(清单4)。

public  interface Concat { 
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个线程,其中包括: 主线程,引用处理程序,完成,附加侦听器等。 灰色的第一部分报告了这些线程对象的对齐方式,以便它可以与偏置锁定一起使用(请注意,所有地址均以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的性能再一次保持稳定。 更有趣的是,尽管比StringBuilder稍慢一些,但StringBuffer的性能比在多核平台上运行时更接近StringBuilder提供的性能。 正是此测试提供了有关该基准实际测量的内容的信息。

在多核世界中,线程之间共享数据具有全新的外观。 所有现代CPU必须利用本地内存缓存来最大程度地减少从内存中获取指令和数据的延迟。 当使用锁时,会导致将内存屏障插入执行路径。 内存屏障是一个信号,告诉CPU必须与所有其他CPU配合才能获取最新值。 为此,CPU将相互通信。 这将导致每个处理器暂停当前正在运行的应用程序线程。 事实证明,多长时间取决于CPU的内存模型。 更为保守的内存模型可能具有更高的线程安全性,但它们也将花费更长的时间在所有内核之间进行协调。 使用Core 2 Duo,第二个内核将固定工作基准的运行时间从3731毫秒增加了。 至6574毫秒。 或176%。 显然,Hotspot可以为我们提供的任何帮助都可以显着改善应用程序的整体性能。

逃逸分析是否有效?

锁定省略是此处明显可用但尚未利用的优化之一。 就是说,锁省略仅在最近才实现,并且它取决于转义分析,而它本身就是在最近才实现的概要分析技术。 公平地说,众所周知,这些技术仅在少数情况下有效。 例如,转义分析适用于简单的循环,这些循环在与本地声明的锁[ http://blog.nirav.name/2007_02_01_archive.html ]同步的块内递增局部变量。 它也可以像Scimark2基准测试的Mont Carlo测试所承诺的那样工作。 (请参阅[ http://math.nist.gov/scimark2/index.html ])。

将逃逸分析进行测试

那么,为什么它不适用于我们的基准呢? 我试图通过内联StringBuffer和StringBuilder的必需方法来降低基准。 我还修改了代码,以试图使转义分析生效。 我期望在某个时候可以取消锁定,并且性能会有所提高。 说通过这个基准测试既令人困惑又令人沮丧,这是不诚实的。 我不得不在编辑器中多次使用ctrl-z来从我以为Elision可以正常工作的版本还原,然后由于某种原因,它不再起作用了。 有时,锁省略只是没有明显原因就开始起作用。

最终,我认识到启用锁定清除和锁定对象的数据大小之间似乎存在某种关系。 运行清单2中的代码时,您可以看到这一点。您可以看到,运行时间没有差异,这意味着DoEscapeAnalysis没有效果。

>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应用锁优化的有效性时,我认为这将花费几个小时的时间,最后我得到了一个有用的博客条目。 但是,正如几乎所有基准测试一样,结果的验证和解释过程持续了数周之久。 它还要求我与其他一些专家合作,这些专家反过来又花了更多时间来验证基准并提供解释。 即使完成了所有这些工作,仍然无法说出很多似乎最有效,最不可行的事情。 尽管本文引用了特定的结果,但它们特定于我的硬件,并且只能推测如果您中的任何人会在系统上看到相同类型的结果。 我还从我认为是相当合理的微基准开始,但最后要花大量时间进行调整,以使我自己和审查基准并没有破例但让Hotspot应用其他不需要的优化的代码的人都满意。 简而言之,该练习比我最初预期的要困难得多。

如果您需要在多核计算机上运行多线程应用程序,并且您非常关心性能,那么您显然需要不断升级到所使用的JDK版本的最新版本。 许多(但不是全部)优化已从最新版本回移植到以前的版本。 您将需要确保启用了所有线程优化。 在6.0中,默认情况下启用它们。 但是,在5.0中,您将需要在命令行上显式设置它们。 如果您在多核计算机上运行单线程应用程序,则可能会通过关闭除第一个内核以外的所有内核来提高应用程序的运行速度。

在较低的级别上,单核上的锁定开销比双核处理器上的锁定开销低得多。 核心间的协调,即内存屏障语义,显然有其开销,如在我的系统上关闭某个核心的情况下运行时基准测试的性能所证明。 显然,我们确实需要线程优化来减少这种开销。 幸运的是,锁粗调(尤其是偏锁)似乎确实对基准性能产生了重大影响。 我希望将转义分析与锁定省略相结合会比它产生更大的影响。 该技术有效,但仅在极少数情况下有效。 公平地说,转义分析仍处于起步阶段,要使这种复杂的工具成熟还需要时间。

总之,最好的基准是您的应用程序在硬件上运行。 本文所能提供的最好的信息是,如果您的多线程性能不如您希望的那样,可能会发生什么。

关于耶隆·博格斯

Jeroen Borgers是Xebia -IT Architects的高级顾问。 Xebia是一家国际IT咨询和项目组织,专门从事企业Java和敏捷开发。 Jeroen帮助客户解决企业Java性能问题,并且是Java Performance Tuning课程的讲师。 自1996年以来,他作为开发人员,架构师,团队负责人,质量官,导师,审计师,性能测试员和调谐器,从事多个行业的Java项目。 自2005年以来,他专门从事绩效任务。

致谢

本文只能在其他几个人的帮助下才能实现。 特别感谢:

Sun的前首席架构师,Cliff Click博士,目前与Azul Systems合作。 帮助分析并指出宝贵的资源。

Java性能权威Kirk Pepperdine; 除了提供帮助和广泛的编辑外,还可以帮助我。

Sun JVM性能团队负责人David Dagastine解释并指出了正确的方向。

我的Xebia同事中有几位是执行基准测试的。

资源资源

在实践Java并发时,Brian Goetz等人。

Java理论与实践:Mustang中的同步优化

逸出分析是否从Java 6逸出

Dave Dice的博客

Java SE 6性能白皮书

清单1。

公共 LockTest {

私有 静态 最终 整数 MAX = 20000000; // 20000000


公共 静态 void 主对象 (String [] args) 抛出 InterruptedException {

//预热方法缓存

for int i = 0; i < MAX ; i ++){

concatBuffer “ Josh” “ James” “ Duke” );

concatBuilder “ Josh” “ James” “ Duke” );

}

系统。 gc ();

线。 睡眠 (1000);


长期 开始=系统。 currentTimeMillis ();

for int i = 0; i < MAX ; i ++){

concatBuffer “ Josh” “ James” “ Duke” );

}

long bufferCost =系统。 currentTimeMillis ()-开始;

系统。 out .println( “ StringBuffer:” + bufferCost + “ ms。” );


系统。 gc ();

线。 睡眠 (1000);


开始=系统。 currentTimeMillis ();

for int i = 0; i < MAX ; i ++){

concatBuilder “ Josh” “ James” “ Duke” );

}

long builderCost =系统。 currentTimeMillis ()-开始;

系统。 out .println( “ StringBuilder:” + builderCost + “ ms。” );

系统。 .println(“的StringBuffer的线程安全的开销:”

+(((bufferCost * 10000 /(builderCost * 100))-100)+ “%\ n” );


}


公共 静态 字符串concatBuffer(String s1,String s2,String s3){

StringBuffer sb = new StringBuffer();

sb.append(s1);

sb.append(s2);

sb.append(s3);

返回 sb.toString();

}


公共 静态 字符串concatBuilder(String s1,String s2,String s3){

StringBuilder sb = new StringBuilder();

sb.append(s1);

sb.append(s2);

sb.append(s3);

返回 sb.toString();

}


}



清单2。

公开 EATest {


私有 静态 最终 整数 MAX = 200000000; // 2亿


公共 静态 最终 void main(String [] args) 引发 InterruptedException {

//预热方法缓存

sumThreadUnsafe ();

sumThreadSafe ();

sumThreadUnsafe ();

sumThreadSafe ();

系统。 out .println( “开始测试” );


漫长的 开始;


开始=系统。 currentTimeMillis ();

sumThreadUnsafe ();

long unsafeCost =系统。 currentTimeMillis ()-开始;

系统。 out .println( “线程不安全:” + unsafeCost + “ ms。” );


开始=系统。 currentTimeMillis ();

sumThreadSafe ();

long safeCost =系统。 currentTimeMillis ()-开始;

系统。 out .println( “线程安全:” + safeCost + “ ms。” );

系统。 .println(“线程安全的开销:”

+(((safeCost * 10000 /(unsafeCost * 100))-100)+ “%\ n” );


}


public static int sumThreadSafe(){

String []名称= new String [] { “ Josh” “ James” “ Duke” “ B” };

ThreadSafeObject ts = new ThreadSafeObject();

int sum = 0;

for int i = 0; i < MAX ; i ++){

sum + = ts.test(names [i%4]);

}

返回 总和

}


public static int sumThreadUnsafe(){

String []名称= new String [] { “ Josh” “ James” “ Duke” “ B” };

ThreadUnsafeObject tus = new ThreadUnsafeObject();

int sum = 0;

for int i = 0; i < MAX ; i ++){

sum + = tus.test(names [i%4]);

}

返回 总和

}


}


最终 课程 ThreadUnsafeObject {

// private int index = 0;

private int count = 0;


私人 字符 [] = 字符 [1];


public int test(String str){

[0] = str.charAt(0);

count = str.length();

退货 计数 ;

}

}


最终 课程 ThreadSafeObject {

private int 索引 = 0; //删除该行,或者仅删除'= 0',它将更快!

private int count = 0;


私人 字符 [] = 字符 [1];


公共 同步 int 测试(字符串str){

[0] = str.charAt(0);

count = str.length();

退货 计数 ;

}

}

翻译自: https://www.infoq.com/articles/java-threading-optimizations-p2/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

java 线程优化

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值