java 线程优化_Java 6线程优化真的有效吗?

java 线程优化

简介-Java 6中的线程优化

Sun,IBM,BEA和其他公司已经给予了极大的关注,以优化各自Java 6虚拟机产品中的锁管理和同步。 诸如偏向锁定,锁定粗化,通过转义分析进行锁定省略和自适应自旋锁定等功能都旨在通过允许在应用程序线程之间进行更有效的共享来提高并发性。 问题是这些功能的复杂性和趣味性。 他们实际上会兑现这些诺言吗? 在这两部分的文章中,我将探讨这些功能,并尝试在单线程基准测试的帮助下回答性能问题。

锁定是悲观的

Java支持的锁定模型(大多数线程库就是这种情况)是一种非常悲观的模型。 如果甚至存在最遥远的危险,即两个或多个线程将以相互干扰的方式利用数据,我们将被迫采用使用锁来防止这种情况发生的严酷解决方案。 然而研究表明,锁极少被争夺(如果有的话)。 翻译,一个请求锁定的线程几乎不需要等待来获取它。 但是,请求锁定的动作将触发一系列动作,这些动作可能导致大量开销的累积,这是无法避免的。

我们确实有一些选择。 考虑使用线程安全的StringBuffer的示例。 问问自己,如果您曾经使用过StringBuffer并知道只能从单个线程访问它,为什么不使用StringBuilder呢?

知道大多数锁都不会争用或很少会争用不是很有帮助,因为即使两个线程可以访问同一数据的可能性很小,那么我们也很可能不得不通过同步来使用锁来保护访问。 只有当我们在运行时环境的上下文中查看锁时,我们才能最终回答这个问题,我们真的需要锁吗? 为了获得该问题的答案,JVM开发人员已开始尝试使用HotSpot和JIT。 通过这项工作,我们现在有了自适应旋转,偏置锁定和两种消除锁定的形式,称为锁定粗化,锁定省略。 在开始进行基准测试之前,让我们花一些时间来复习这些功能,以便我们都了解它们的工作原理。

转义分析-锁定省略说明

转义分析是对正在运行的应用程序中所有引用的范围的检查。 作为HotSpot探查器工作的正常部分进行分析。 如果HotSpot(通过Escape Analysis)可以确定对某个对象的引用仅限于本地范围,而这些引用都不能“逃逸”到更大的范围,则它可以指示JIT应用许多运行时优化。 一种这样的优化称为锁省略。 当对锁的引用限于某个局部范围时,这意味着只有创建该锁的线程才可以访问该锁。 在这些条件下,将永远不会争用同步块中的值。 这意味着我们从不真正需要该锁,并且可以安全地将其删除(省略)。 请考虑以下方法:

public String concatBuffer(String s1, String s2, String s3) {,
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}

图1.使用本地StringBuffer的字符串连接

如果我们检查变量sb,我们可以快速确定它仅存在于concatBuffer方法的范围内。 此外,对sb的引用永远不会“脱离”声明它的范围。 因此,没有其他线程可以访问我们的sb副本。 掌握了这些知识后,我们知道可以取消保护某人的锁。

从表面上看,锁省略似乎使我们能够编写线程安全的代码,而在确实不需要它的情况下,无需使用任何同步代价。 如果确实可行,我们将使用基准进行调查。

偏向锁定说明

有偏锁是从以下观察得出的:大多数锁在其生命周期内永远不会被多个线程访问。 在极少数情况下,多个线程确实共享数据时,很少会争用该访问。 要根据观察结果了解有偏锁的优势,我们首先需要回顾如何获取锁(监视器)。

获取锁是两步舞。 首先,您需要获得租赁。 一旦有了手头的租约,您就可以自由地抓住锁。 为了获得租约,线程需要执行昂贵的原子指令。 传统上释放锁会释放租约。 根据我们的观察,似乎在线程正在同步代码块上循环的情况下,我们似乎应该能够优化访问。 我们可以做的一件事是粗化锁以包括循环。 这将导致线程访问锁一次,而不是循环计数次数。 但是,这不是一个好的解决方案,因为它可能会使其他线程无法获得公平的访问权限。 一个更明智的解决方案是将锁偏向循环线程。

将锁偏向一个线程意味着不需要该线程来释放锁上的租约。 因此,随后的锁获取便宜得多。 仅当另一个线程尝试获取锁时,该线程才释放租约。 Java 6 HotSpot / JIT实现默认情况下针对偏向锁定进行了优化。

锁定粗化说明

另一个线程优化是锁粗化或合并。 当相邻的同步块可以合并为一个同步块时,会发生锁定粗化。 此主题的一种变体是将多个同步方法组合为一个。 如果所有方法都使用相同的锁定对象,则可以应用此优化。 考虑图2所示的示例。

public static String concatToBuffer(StringBuffer sb, String s1, String s2, String s3) {
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}

图2.使用非本地StringBuffer的字符串连接

在这种情况下,StringBuffer具有非本地范围,可以被多个线程访问。 因此,转义分析将确定无法安全地消除其锁。 如果恰好只有一个线程访问该锁,则可以应用有偏锁。 有趣的是,可以根据竞争锁的线程数来做出粗化的决定。 在这种情况下,实例锁获得了四次:追加方法获得三次,toString方法获得三次,一次又一次。 第一步是内联方法。 然后,我们可以用包围整个方法主体的单个调用替换所有四个调用以获取锁。

最终的结果是,我们将得到更长的关键部分,这可能导致其他线程停止运行并降低吞吐量。 因此,不会将循环内的锁粗化为包括循环构造。

线程暂停与旋转

当一个线程等待另一个线程释放锁时,通常它会被操作系统挂起。 挂起线程需要操作系统经常在消耗时间之前将其交换出CPU。 当具有锁的线程离开临界区时,需要唤醒挂起的线程。 该线程将需要重新安排,并将上下文切换回CPU。 所有这些活动可能会对JVM,操作系统和硬件造成额外的压力。

在这种情况下有用的观察是: 锁通常保持很短的时间。 这意味着,如果我们稍等片刻,便可以在不暂停的情况下获得锁。 要等待我们所做的一切,就是将线程置于繁忙循环中(旋转)。 此技术称为自旋锁定。

在锁定持续时间很短的情况下,自旋锁定效果很好。 另一方面,如果将锁保持更长的时间,则自旋是浪费的,因为自旋线程会消耗CPU而不做任何有用的事情。 在JDK 1.4.2中引入时,自旋锁定被分为两个阶段,在被暂停之前,自旋进行10次迭代(默认值)。

自适应旋转

JDK 1.6引入了自适应旋转。 使用自适应旋转时,旋转的持续时间不再固定,而是由基于对同一锁的先前旋转尝试和锁所有者状态的策略确定。 如果在最近的历史记录中对该相同的锁对象成功进行了旋转,并且持有该锁的线程正在运行,则旋转很可能会再次成功。 然后将应用相对较长的时间,例如100次迭代。 另一方面,如果旋转不太可能成功,则将其完全省去,从而不会浪费任何CPU周期。

StringBuffer与StringBuilder基准测试

决定如何精确衡量所有这些优化的效果的道路并不顺利。 首先是基准应该是什么样的问题? 为了回答这个问题,我决定看看人们一直在他们的代码中使用的一些常见习语。 突出的一件事是一个古老的问题,即使用StringBuffer代替String可以节省多少成本。

熟悉的建议是: 如果要对其进行突变,请使用StringBuffer而不是String。 建议的原因很清楚。 字符串是不可变的,如果我们正在做需要突变的工作,则StringBuffer是一种成本较低的选择。 有趣的是,该建议未能识别出StringBuffer的新(自1.5版本起)的未同步表亲StringBuilder。 由于StringBuilder和StringBuffer之间的唯一区别是同步,因此似乎衡量两者性能差异的基准似乎可以揭示同步的成本。 任务从第一个问题开始,即无竞争锁定的成本是多少。

基准测试的本质(如清单1所示)是采用几个字符串并将它们连接在一起。 基础缓冲区的初始容量足以容纳要连接的三个字符串。 这使我们可以将关键部分的工作量减到最少,这会使度量偏向于同步成本。

基准结果

下面是EliminateLocks,UseBiasedLocking和DoEscapeAnalysis三个选项的合理组合的结果。

图3.基准结果

讨论结果

使用不同步的StringBuilder的目的是提供性能的基准度量。 我还想看看优化是否会对StringBuilder的性能产生影响。 可以看出,StringBuilder的性能在整个练习过程中保持不变。 因为这些标志直接针对优化锁的使用,所以此结果是预期的。 在性能范围的另一端,我们可以看到在没有任何优化的情况下使用同步StringBuffer的速度慢了大约3倍。

在图3中显示的结果中,从左到右移动,我们看到性能的显着提高可以归因于EliminateLocks。 但是,与偏置锁定提供的提升相比,这种提升显得微不足道。 实际上,除C列外,每次打开偏置锁定的运行都会带来几乎相同的性能提升。 但是C列是什么?

在处理原始数据的过程中,注意到在6中运行1次所花费的时间明显更长。 差异很大,以至于工作台似乎报告了两个完全不同的优化。经过一番思考,我决定分别报告较高和较低的值(运行B和C)。 除非进行更深入的研究,否则我只能推测可以应用多个可能的优化(很可能是两个),并且存在某种竞态条件,其中大多数情况下偏向锁定会获胜,但并非所有条件都可以胜出。时间。 如果其他优化获胜,则可以防止或延迟使用有偏锁。

奇怪的结果是逃逸分析产生的结果。 考虑到该基准测试的单线程性质,我完全希望Escape Analysis能够消除锁,从而使StringBuffer的性能与StringBuilder的性能相同。 很显然,这没有发生。 另一个问题; 我的机器上运行的时间因运行而异。 为了进一步弄清水,我让几个同事在他们的系统上进行了测试。 在某些情况下,优化并不能使速度大大提高。

过早的结论

尽管图3中显示的结果比我期望的要少,但它们确实表明优化确实消除了大多数锁定开销。 但是,由我的同事进行的测试运行会产生不同的结果,这似乎对结果的准确性提出了挑战。 是衡量锁定开销的基准还是我们的结论为时过早,还有其他事情吗? 在本文的第2部分中,我们将更深入地研究此基准,以尝试回答这些问题。 在此过程中,我们将发现获得结果很容易。 确定结果是否已经回答了我们提出的问题,这是一个复杂得多的任务。

public class  LockTest {
private static final int MAX = 20000000; // 20 million

public static void main(String[] args) throws InterruptedException {
// warm up the method cache
for (int i = 0; i < MAX ; i++) {
concatBuffer( "Josh", "James", "Duke" );
concatBuilder( "Josh", "James", "Duke" );
}

System.gc();
Thread.sleep(1000);

System. out .println( "Starting test" );
long start = System.currentTimeMillis();
for (int i = 0; i < MAX ; i++) {
concatBuffer( "Josh", "James", "Duke" );
}
long bufferCost = System.currentTimeMillis() - start;
System. out .println( "StringBuffer: " + bufferCost + " ms." );

System.gc();
Thread.sleep(1000);

start = System.currentTimeMillis();
for (int i = 0; i < MAX ; i++) {
concatBuilder( "Josh", "James", "Duke" );
}
long builderCost = System.currentTimeMillis() - start;
System. out .println( "StringBuilder: " + builderCost + " ms.");
System. out .println( "Thread safety overhead of StringBuffer: "
+ ((bufferCost * 10000 / (builderCost * 100)) - 100) + "%\n");

}

public static String concatBuffer(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}

public static String concatBuilder(String s1, String s2, String s3) {
StringBuilder sb = new StringBuilder();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}

}

第二部分中 ,我们将更深入地研究用于验证基准测试结果是否有效的技术,然后我们将回答真正的问题。

运行基准

我在装有Java 1.6.0_04的Intel Core 2 Duo的32位Windows Vista笔记本电脑上运行了该基准测试。 请注意,所有优化都在服务器虚拟机中实现。 那不是我平台上的默认虚拟机。 它甚至不能从JRE中获得,而只能从JDK中获得。 为了确保使用服务器虚拟机,我在命令行上指定了-server选项。 其他选项是:

  • -XX:+ DoEscapeAnalysis,默认关闭
  • -XX:+ UseBiasedLocking,默认情况下处于启用状态
  • -XX:+ EliminateLocks,默认情况下处于启用状态

要运行基准测试,请编译源代码,然后使用类似于以下内容的命令行

java-server -XX:+DoEscapeAnalysis LockTest

关于耶隆·博格斯

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性能白皮书

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

java 线程优化

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值