这里写自定义目录标题
CAD 和 Fetch And Add 实例
我们以AtomicInteger里面的incrementAndGet方法为例子,该方法是给变量值加1.
在jdk1.7中源码如下:
public final long incrementAndGet() {
for (;;) {
long current = get();
long next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
在jdk1.8则是
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
jdk1.7 用的是不断循环的CAS方法,而jdk1.8只用了一个方法搞定,其实是运用了cpu x86的 LOCK XADD 指令。后者的性能优于前者。两者的对比,oracle官网的这篇文章介绍的非常详细。以下是我对这篇文章的翻译。
https://blogs.oracle.com/dave/atomic-fetch-and-add-vs-compare-and-swap
译文
在许多情况下,原子Fetch and add指令可能产生比传统Load更好的性能;Φ; CAS循环,其中CAS是原子比较和交换指令。 x86架构提供了了LOCK:XADD指令,我将在下面的讨论中使用它。 (如果您不需要关系到原始值,也可以使用LOCK:INC或LOCK:ADD而不是LOCK:XADD)。
-
CAS是“乐观锁”并且可能失败,而XADD则不承认。使用XADD时,没有明确的远程干扰窗口,因此不需要重试循环。可以说,XADD具有更好的进度属性,因为底层XADD没有隐式循环判断,但即使在这种情况下,窗口也会比使用Load;Φ; CAS更窄。
-
如果使用典型的Load;Φ; CAS,这是基于snoop的缓存一致性,则Load可能会导致读取共享总线事务使底层缓存行进入S(Share)或E(Exclude)状态。遵守缓存一致性协议的CAS可以引起另一总线事务以线路升级到M状态。因此,在最坏的情况下,CAS可能会导致两个总线事务.但XADD通常会将线路直接驱动到M(Modified)状态,只需要一个总线事务。当然,可以通过推测值方法,在没有任何先前加载的情况下,直接进行CAS的操作。 (我经常在本机HotSpot代码中使用它)。此外,复杂的CPU可以执行相关推测并积极地将目标线设置为M(Modified)状态。最后,在某些情况下,您可以在加载之前执行prefetch-for-write(PREFETCHW)指令,以避免升级事务。但是这种方法必须谨慎应用,因为在某些情况下它可能弊大于利。鉴于此,XADD在可用情况下具有优势。
-
让我们假设您尝试使用通常的Load; INC; CAS循环来增加变量。当CAS失败的频率很大时候,可以发现退出循环的分支指令(通常在无争用或轻度争用下)开始预测我们停留在循环中的故障路径。因此,当CAS最终成功时,在尝试退出循环时会导致分支错误预测。对于具有深度管道和大量无序处理信息机制的处理器而言,这可能会非常痛苦。通常情况下,执行一段代码,并不需要长时间停顿。相关地,当CAS开始频繁失败,分支指令开始预测控制保持在循环中,循环运行得越快,就更快地循环到CAS。通常,我们希望循环执行中有一些回退。在轻负载且不经常发生故障的情况下,再次循环的错误预测可能是一种有用的隐含回退。但是在更高的负荷下,我们失去了因为错误错误预测带来的隐含回退的好处。由于XADD没有循环,自然也没有以上的问题。
尽管CAS具有更高(无限)的consensus number,但有时候fetch-and-add–其consensus number仅为2 - 是更适合的。