简析锁粗化(Lock coarsening explained)
另一种线程优化方式是锁粗化(或合并merging)当多个彼此靠近的同步块可以合并到一起形成一个同步块的时候就会进行锁粗化该方法还有一种变体可以把多个同步方法合并为一个方法如果所有方法都用一个锁对象就可以尝试这种方法考虑图中的实例
public static String concatToBuffer(StringBuffer sb String s String s String s) {
sbappend(s);
sbappend(s);
sbappend(s);
return
}
在这个例子中StringBuffer的作用域是非局部的可以被多个线程访问所以逸出分析会判断出StringBuffer的锁不能安全地被忽略如果锁刚好只被一个线程访问则可以使用偏向锁有趣的是是否进行锁粗化与竞争锁的线程数量是无关的在上面的例子中锁的实例会被请求四次前三次是执行append方法最后一次是执行toString方法紧接着前一个首先要做的是将这种方法进行内联然后我们只需执行一次获取锁的操作(为整个方法)而不必像以前一样获取四次锁了
这种做法带来的真正效果是我们获得了一个更长的临界区它可能导致其他线程受到拖延从而降低吞吐量正因为这些原因一个处于循环内部的锁是不会被粗化到包含整个循环体的
线程挂起 vs 自旋(Thread suspending versus spinning)
在一个线程等待另外一个线程释放某个锁的时候它通常会被操作系统挂起操作在挂起一个线程的时候需要将它换出CPU而通常此时线程的时间片还没有使用完当拥有锁的线程离开临界区的时候挂起的线程需要被重新唤醒然后重新被调用并交换上下文回到CPU调度中所有这些动作都会给JVMOS和硬件带来更大的压力
在这个例子中如果注意到下面的事实会很有帮助锁通常只会被占有很短的一段时间这就是说如果能够等上一会儿我们可以避免挂起线程的开销为了让线程等待我们只需将线程执行一个忙循环(自旋)这项技术就是所谓的自旋锁
当锁被占有的时间很短时自旋锁的效果非常好另一方面如果锁被占有很长时间那么自旋的线程只会消耗CPU而不做任何有用的工作因此带来浪费自从JDK 中引入自旋锁以来自旋锁被分为两个阶段自旋十个循环(默认值)然后挂起线程
自适应自旋锁(Adaptive spinning)
JDK 中引入了自适应自旋锁自适应意味着自旋的时间不再固定了而是取决于一个基于前一次在同一个锁上的自旋时间以及锁的拥有者的状态如果在同一个锁对象上自旋刚刚成功过并且持有锁的线程正在运行中那么自旋很有可能再次成功进而它将被应用于相对更长的时间比如个循环另一方面如果自旋很少发生过它将被遗弃避免浪费任何CPU周期
StringBuffer vs StringBuilder的基准测试
但是要想设计出一种方法来判断这些巧妙的优化方法到底多有效这条路并不平坦首要的问题就是如何设计基准测试为了找到问题的答案我决定去看看人们通常在代码中运用了哪些常见的技巧我首先想到的是一个非常古老的问题使用StringBuffer代替String可以减少多少开销?
一个类似的建议是如果你希望字符串是可变的就应该使用StringBuffer这个建议的缘由是非常明确的String是不可变的但如果我们的工作需要字符串有很多变化StringBuffer将是一个开销较低的选择有趣的是在遇到JDK 中的StringBuilder(它是StringBuffer的非同步版本)后这条建议就不灵了由于StringBuilder与 StringBuffer之间唯一的不同在于同步性这似乎说明测量两者之间性能差异的基准测试必须关注在同步的开销上我们的探索从第一个问题开始非竞争锁的开销如何?
这个基准测试的关键(如清单所示)在于将大量的字符串拼接在一起底层缓沖的初始容量足够大可以包含三个待连接的字符串这样我们可以将临界区内的工作最小化进而重点测量同步的开销
基准测试的结果
下图是测试结果包括EliminateLocksUseBiasedLocking和DoEscapeAnalysis的不同组合
图 基准测试的结果
[] [] [] []