Java 中的旋转测试和设置与旋转读取

spin-on-test-and-set vs spin-on-read in Java

背景

我正在使用开源 off-heap cache implementation, ohc

最近,我在 github 上发现了一个 pull request,建议

replace spin-on-compare-and-set with spin-on-read.

Here是代码的变化,只增加了一行while(lockFieldUpdater.get(this) != 0L),类似于

    while (true)
    {
        if (lockFieldUpdater.compareAndSet(this, 0L, t))
            return true;

            // while(lockFieldUpdater.get(this) != 0L)
            Thread.yield();
    }

基准性能

我编译它并使用 benchmark tool 测试它:

线上表现

然后我用在生产上,原来读取的平均时间成本是35000纳秒,新版本只需要10000纳秒

问题

这两种实现有什么区别?为什么在这种情况下读取测试要快得多?

为了理解性能提高的原因,了解一点缓存一致性协议是有好处的。原始版本仅依赖于繁重的读-修改-写操作,如果失败则放弃。这是一个笨拙的做法,因为 CAS 操作会产生相当多的缓存一致性流量,以获得缓存行的所有权,使其他核心上的副本无效等。随着线程数量的增加,这种天真的方法会导致大量争用。

修改后的版本是对原始方法的改进,因为它可以更好地同步线程的操作。通过确保每个线程都将在其自己的缓存副本上旋转,只有在本地副本失效(在另一个核心中修改)后,该线程才会再次被允许尝试 CAS。

这与 TATAS locks 是对简单 TAS 锁的改进的原因非常相似。

至于为什么您的本地基准测试显示加速约为 6% 而您的生产服务器看到加速约为 3.5 倍,可能有几个原因可以解释。

  1. 您的生产服务器从局部变量上旋转受益匪浅,因为跨 NUMA 节点的内存访问会严重影响性能。
  2. TAS锁和TATAS锁的性能都会随着争用锁的线程数的增加而下降。但是 TATAS 锁降级比 TAS 锁慢。这篇博客文章 Test-and-set spinlocks 有一个很好的图表来说明这一点。也许您的本地基准测试太小而看不到任何显着改进?