快捷搜索:

Java 6中的线程优化真的有效么?

先容 — Java 6中的线程优化

Sun、IBM、BEA和其他公司在各自实现的Java 6虚拟机上都花费了大年夜量的精力优化锁的治理和同步。诸如方向锁(biased locking)、锁粗化(lock coarsening)、由逸出(escape)阐发孕育发生的锁省略、自适应自旋锁(adaptive spinning)这些特点,都是经由过程在利用法度榜样线程之间更高效地共享数据,从而前进并发效率。只管这些特点都是成熟且有趣的,然则问题在于:它们的允诺真的能实现么?在这篇由两部分组成的文章里,我将一一商量这些特点,并考试测验在单一线程基准的帮忙下,回答关于机能的问题。

消极锁模型

Java支持的锁模型绝对是消极锁(着实,大年夜多半线程库都是如斯)。假如有两个或者更多线程应用数据时会彼此滋扰,这种极小的风险也会逼迫我们采纳异常严峻的手段防止这种环境的发生——应用锁。然而钻研注解,锁很少被占用。也便是说,一个造访锁的线程很少必须等待来获取它。然则哀求锁的动作将会触发一系列的动作,这可能导致严重的系统开销,这是弗成避免的。

我们切实着实还有其他的选择。举例来说,斟酌一下线程安然的StringBuffer的用法。问问你自己:是否你曾经明知道它只能被一个线程安然地造访,照样坚持应用StringBuffer,为什么不用StringBuilder代替呢?

知道大年夜多半的锁都不存在竞争,或者很少存在竞争的事实对我们感化并不大年夜,由于纵然是两个线程造访相同数据的概率异常低,也会逼迫我们应用锁,经由过程同步来保护被造访的数据。“我们真的必要锁么?”这个问题只有在我们将锁放在运行时情况的高低文中察看之后,才能终极给出谜底。为了找到问题的谜底,JVM的开拓者已经开始在HotSpot和JIT长进行了很多的实验性的事情。现在,我们已经从这些事情中得到了自适应自旋锁、方向锁和以及两种要领的锁打消(lock elimination)——锁粗化和锁省略(lock elision)。在我们开始进行基准测试曩昔,先来花些光阴回首一下这些特点,这样有助于理解它们是若何事情的。

逸出阐发 — 简析锁省略(Escape analysis - lock elision explained)

逸出阐发是对运行中的利用法度榜样中的整个引用的范围所做的阐发。逸出阐发是HotSpot阐发事情的一个组成部分。假如HotSpot(经由过程逸出阐发)能够判断出指向某个工具的多个引用被限定在局部空间内,并且所有这些引用都不能“逸出”到这个空间以外的地方,那么HotSpot会要求JIT进行一系列的运行时优化。此中一种优化便是锁省略(lock elision)。假如锁的引用限定在局部空间中,阐明只有创建这个锁的线程才会造访该锁。在这种前提下,同步块中的值永世不会存在竞争。这意味这我们永世弗成能真的必要这把锁,它可以被安然地轻忽掉落。斟酌下面的措施:

publicString 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的所有引用永世不会“逸出”到 concatBuffer措施之外,即声明它的那个措施。是以其他线程无法造访当火线程的sb副本。根据我们刚先容的常识,我们知道用于保护sb的锁可以轻忽掉落。

从外面上看,锁省略彷佛可以容许我们不必忍受同步带来的包袱,就可以编写线程安然的代码了,条件是在同步切实着实是多余的环境下。锁省略是否真的能发挥感化呢?这是我们在后面的基准测试中将要回答的问题。

简析方向锁(Biased locking explained)

大年夜多半锁,在它们的生命周期中,从来不会被多于一个线程所造访。纵然在极少数环境下,多个线程真的共享数据了,锁也不会发生竞争。为了理解方向锁的上风,我们首先必要回首一下若何获取锁(监视器)。

获取锁的历程分为两部分。首先,你必要得到一份左券.一旦你得到了这份左券,就可以自由地拿到锁了。为了得到这份左券,线程必须履行一个价值昂贵的原子指令。开释锁同时就要开释左券。根据我们的察看,我们彷佛必要对一些锁的造访进行优化,比如线程履行的同步块代码在一个轮回体中。优化的措施之一便是将锁粗化,以包孕全部轮回。这样,线程只造访一次锁,而不必每次进入轮回时都进行造访了。然则,这并非一个很好的办理规划,由于它可能会阴碍其他线程合法的造访。还有一个更合理的规划,即将锁方向给履行轮回的线程。

将锁方向于一个线程,意味着该线程不必要开释锁的左券。是以,随后获取锁的时刻可以不那么昂贵。假如另一个线程在考试测验获取锁,那么轮回线程只必要开释左券就可以了。Java 6的HotSpot/JIT默认环境下实现了方向锁的优化。

简析锁粗化(Lock coarsening explained)

另一种线程优化要领是锁粗化(或合并,merging)。当多个彼此接近的同步块可以合并到一路,形成一个同步块的时刻,就会进行锁粗化。该措施还有一种变体,可以把多个同步措施合并为一个措施。假如所有措施都用一个锁工具,就可以考试测验这种措施。斟酌图2中的实例。

public static String concatToBuffer(StringBuffer sb, String s1, String s2, String s3) {

sb.append(s1);

sb.append(s2);

sb.append(s3);

return

}

图2. 应用非局部的StringBuffer连接字符串

在这个例子中,StringBuffer的感化域长短局部的,可以被多个线程造访。以是逸出阐发会判断出StringBuffer的锁不能安然地被轻忽。假如锁刚好只被一个线程造访,则可以应用方向锁。有趣的是,是否进行锁粗化,与竞争锁的线程数量是无关的。在上面的例子中,锁的实例会被哀求四次:前三次是履行append措施,着末一次是履行toString措施,紧接着前一个。首先要做的是将这种措施进行内联。然后我们只需履行一次获取锁的操作(为全部措施),而不必像曩昔一样获取四次锁了。

这种做法带来的真正效果是我们得到了一个更长的临界区,它可能导致其他线程受到迁延从而低落吞吐量。正由于这些缘故原由,一个处于轮回内部的锁是不会被粗化到包孕全部轮回体的。

线程挂起 vs. 自旋(Thread suspending versus spinning)

在一个线程等待别的一个线程开释某个锁的时刻,它平日会被操作系统挂起。操作在挂起一个线程的时刻必要将它换出CPU,而平日此时线程的光阴片还没有应用完。当拥有锁的线程脱离临界区的时刻,挂起的线程必要被从新唤醒,然后从新被调用,并互换高低文,回到CPU调整中。所有这些动作都邑给JVM、OS和硬件带来更大年夜的压力。

在这个例子中,假如留意到下面的事实会很有赞助:锁平日只会被占领很短的一段光阴。这便是说,假如能够等上一下子,我们可以避免挂起线程的开销。为了让线程等待,我们只需将线程履行一个忙轮回(自旋)。这项技巧便是所谓的自旋锁。

当锁被占领的光阴很短时,自旋锁的效果异常好。另一方面,假如锁被占领很长光阴,那么自旋的线程只会耗损CPU而不做任何有用的事情,是以带来挥霍。自从JDK 1.4.2中引入自旋锁以来,自旋锁被分为两个阶段,自旋十个轮回(默认值),然后挂起线程。

自适应自旋锁(Adaptive spinning)

JDK 1.6中引入了自适应自旋锁。自适应意味着自旋的光阴不再固定了,而是取决于一个基于前一次在同一个锁上的自旋光阴以及锁的拥有者的状态。假如在同一个锁工具上,自旋刚刚成功过,并且持有锁的线程正在运行中,那么自旋很有可能再次成功。进而它将被利用于相对更长的光阴,比如100个轮回。另一方面,假如自旋很少发生过,它将被抛弃,避免挥霍任何CPU周期。

StringBuffer vs. StringBuilder的基准测试

然则要想设计出一种措施来判断这些奇妙的优化措施到底多有效,这条路并不平坦。重要的问题便是若何设计基准测试。为了找到问题的谜底,我抉择去看看人们平日在代码中运用了哪些常见的技术。我首先想到的是一个异常古老的问题:应用StringBuffer代替String可以削减若干开销?

一个类似的建议是,假如你盼望字符串是可变的,就应该应用StringBuffer。这个建议的启事是异常明确的。String是弗成变的,但假如我们的事情必要字符串有很多变更,StringBuffer将是一个开销较低的选择。有趣的是,在碰到JDK 1.5中的StringBuilder(它是StringBuffer的非同步版本)后,这条建议就不灵了。因为StringBuilder与 StringBuffer之间独一的不合在于同步性,这彷佛阐明,丈量两者之间机能差异的基准测试必须关注在同步的开销上。我们的探索从第一个问题开始,非竞争锁的开销若何?

这个基准测试的关键(如清单1所示)在于将大年夜量的字符串拼接在一路。底层缓冲的初始容量足够大年夜,可以包孕三个待连接的字符串。这样我们可以将临界区内的事情最小化,进而重点丈量同步的开销。

基准测试的结果

下图是测试结果,包括EliminateLocks、UseBiasedLocking和DoEscapeAnalysis的不合组合。

图3. 基准测试的结果

关于结果的评论争论

之以是应用非同步的StringBuilder,是为了供给一个丈量机能的基线。我也想懂得一下各类优化是否真的能够影响StringBuilder的机能。正如我们所看到的,StringBuilder的机能可以维持在一个不变的吞吐量水平上。由于这些技巧的目标在于锁的优化,是以这个结果相符预期。在机能测试的另一栏中我们也可以看到,应用没有任何优化的同步的StringBuffer,其运行效率比StringBuilder大年夜概要慢三倍。

仔细察看图3的结果,我们可以留意到从左到右机能有必然的前进,这可以归功于EliminateLocks。不过,这些机能的提升比起方向锁来说又显得有些苍白。事实上,除了C列以外,每次运行时假如开启方向锁终极都邑供给大年夜致相同的机能提升。然则,C列是怎么回事呢?

在处置惩罚最初的数据的历程中,我留意到有一项测试在六个测试中要花费非分特别长的光阴。因为结果的非常相称显着,是以基准测试彷佛在申报两个完全不合的优化行径。颠末一番斟酌,我抉择同时展示出高值和低值(B列和C列)。因为没有更深入的钻研,我只能预测这里利用了一种以上的优化(很可能是两种),并且存在一些竞争前提,方向锁大年夜多时刻会取胜,但不非总能取胜。假如另一种优化占优了,那么方向锁的效果要么被抑制,要么就被延迟了。

这种稀罕的征象是逸出阐发导致的。明确了这个基准测试的单线程化的本色后,我等候着逸出阐发会打消锁,从而将StringBuffer的机能提到了与 StringBuilder相同的水平。然则很显着,这并没有发生。还有别的一个问题;在我的机械上,每一次运行的光阴片分配都不尽相同。更为繁杂的是,我的几位同事在他们的机械上运行这些测试,获得的结果更纷乱了。在有些时刻,这些优化并没有将法度榜样提速那么多。

前期的结论

只管图3列出的结果比我所期望的要少,但确凿可以从中看出各类优化能够撤除锁孕育发生的大年夜部分开销。然则,我的同事在运行这些测试时孕育发生了不合的结果,这彷佛对测试结果的真实性提出了寻衅。这个基准测试真的丈量锁的开销了么?我们的结论成熟么?或者还有没有其他的环境?在本文的第二部分里,我们将会深入钻研这个基准测试,力图回答这些问题。在这个历程中,我们会发明获取结果并不艰苦,艰苦的是判断出这些结果是否可以回答前面提出的问题。

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();

}

}

运行基准测试

我运行这个测试的情况是:32位的Windows Vista条记本电脑,配有Intel Core 2 Duo,应用Java 1.6.0_04。请留意,所有的优化都是在Server VM上实现的。但这在我的平台上不是默认的VM,它以致不能在JRE中应用,只能在JDK中应用。为了确保我应用的是Server VM,我必要在敕令行上打开-server选项。其他的选项包括:

-XX:+DoEscapeAnalysis, off by default

-XX:+UseBiasedLocking, on by default

-XX:+EliminateLocks, on by default

编译源代码,运行下面的敕令,可以启动测试:

java-server -XX:+DoEscapeAnalysis LockTest关于Jeroen Borgers

Jeroen Borger是Xebia的资深咨询师。Xebia是一家国际IT咨询与项目组织公司,专注于企业级Java和敏捷开拓。Jeroen赞助他的客户霸占企业级Java系统的机能问题,他同时照样Java机能调试课程的讲师。他在从1996年开始就可以在不合的Java项目中事情,担负过开拓者、架构师、团队lead、质量认真人、顾问、审核员、机能测试和调试员。他从2005年开始专注于机能问题。

您可能还会对下面的文章感兴趣: