介绍 — Java 中的线程优化
SunIBMBEA和其他公司在各自实现的Java 虚拟机上都花费了大量的精力优化锁的管理和同步诸如偏向锁(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 s
String s
String s
) {
StringBuffer sb = new StringBuffer();
sbappend(s);
sbappend(s);
sbappend(s);
return sbtoString();
}
如果我们观察变量sb很快就会发现它仅仅被限制在concatBuffer方法内部了进一步说到sb的所有引用永远不会逸出到 concatBuffer方法之外即声明它的那个方法因此其他线程无法访问当前线程的sb副本根据我们刚介绍的知识我们知道用于保护sb的锁可以忽略掉
从表面上看锁省略似乎可以允许我们不必忍受同步带来的负担就可以编写线程安全的代码了前提是在同步的确是多余的情况下锁省略是否真的能发挥作用呢?这是我们在后面的基准测试中将要回答的问题
简析偏向锁(Biased locking explained)
大多数锁在它们的生命周期中从来不会被多于一个线程所访问即使在极少数情况下多个线程真的共享数据了锁也不会发生竞争为了理解偏向锁的优势我们首先需要回顾一下如何获取锁(监视器)
获取锁的过程分为两部分首先你需要获得一份契约一旦你获得了这份契约就可以自由地拿到锁了为了获得这份契约线程必须执行一个代价昂贵的原子指令释放锁同时就要释放契约根据我们的观察我们似乎需要对一些锁的访问进行优化比如线程执行的同步块代码在一个循环体中优化的方法之一就是将锁粗化以包含整个循环这样线程只访问一次锁而不必每次进入循环时都进行访问了但是这并非一个很好的解决方案因为它可能会妨碍其他线程合法的访问还有一个更合理的方案即将锁偏向给执行循环的线程
将锁偏向于一个线程意味着该线程不需要释放锁的契约因此随后获取锁的时候可以不那么昂贵如果另一个线程在尝试获取锁那么循环线程只需要释放契约就可以了Java 的HotSpot/JIT默认情况下实现了偏向锁的优化
[] [] [] []