CAS与同步表现

Eug*_*ene 11 atomic compare-and-swap java-8 atomic-long jmh

我已经有这个问题了很长一段时间,试图阅读大量资源并了解正在发生的事情 - 但我仍然未能很好地理解为什么事情就是这样.

简单地说,我想测试如何CAS将执行VS synchronized在争,而不是环境.我提出了这个JMH测试:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@State(Scope.Benchmark)
public class SandBox {

    Object lock = new Object();

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder().include(SandBox.class.getSimpleName())
                .jvmArgs("-ea", "-Xms10g", "-Xmx10g")
                .shouldFailOnError(true)
                .build();
        new Runner(opt).run();
    }

    @State(Scope.Thread)
    public static class Holder {

        private long number;

        private AtomicLong atomicLong;

        @Setup
        public void setUp() {
            number = ThreadLocalRandom.current().nextLong();
            atomicLong = new AtomicLong(number);
        }
    }

    @Fork(1)
    @Benchmark
    public long sync(Holder holder) {
        long n = holder.number;
        synchronized (lock) {
            n = n * 123;
        }

        return n;
    }

    @Fork(1)
    @Benchmark
    public AtomicLong cas(Holder holder) {
        AtomicLong al = holder.atomicLong;
        al.updateAndGet(x -> x * 123);
        return al;
    }

    private Object anotherLock = new Object();

    private long anotherNumber = ThreadLocalRandom.current().nextLong();

    private AtomicLong anotherAl = new AtomicLong(anotherNumber);

    @Fork(1)
    @Benchmark
    public long syncShared() {
        synchronized (anotherLock) {
            anotherNumber = anotherNumber * 123;
        }

        return anotherNumber;
    }

    @Fork(1)
    @Benchmark
    public AtomicLong casShared() {
        anotherAl.updateAndGet(x -> x * 123);
        return anotherAl;
    }

    @Fork(value = 1, jvmArgsAppend = "-XX:-UseBiasedLocking")
    @Benchmark
    public long syncSharedNonBiased() {
        synchronized (anotherLock) {
            anotherNumber = anotherNumber * 123;
        }

        return anotherNumber;
    }

}
Run Code Online (Sandbox Code Playgroud)

结果如下:

Benchmark                                           Mode  Cnt     Score      Error  Units
spinLockVsSynchronized.SandBox.cas                  avgt    5   212.922 ±   18.011  ns/op
spinLockVsSynchronized.SandBox.casShared            avgt    5  4106.764 ± 1233.108  ns/op
spinLockVsSynchronized.SandBox.sync                 avgt    5  2869.664 ±  231.482  ns/op
spinLockVsSynchronized.SandBox.syncShared           avgt    5  2414.177 ±   85.022  ns/op
spinLockVsSynchronized.SandBox.syncSharedNonBiased  avgt    5  2696.102 ±  279.734  ns/op
Run Code Online (Sandbox Code Playgroud)

在非共享的情况下,CAS迄今为止速度更快,这是我所期望的.但在共同的情况下,事情是相反的 - 这是我无法理解的.我不认为这与偏置锁定有关,因为在线程持有锁5秒(AFAIK)之后会发生这种情况并且这不会发生并且测试只是证明.

老实说,我希望这只是我的测试是错误的,并且有jmh专业知识的人会出现并指出我在这里的错误设置.

Hol*_*ger 24

主要的误解是假设您正在比较" CASsynchronized".鉴于JVM实施的复杂程度synchronized,您将CAS基于a 的算法AtomicLong的性能与CAS用于实现的基于算法的性能进行比较synchronized.

类似于Lock,对象监视器的内部信息基本上由一个int状态组成,该状态告知它是否已被拥有以及嵌套的频率,对当前所有者线程的引用以及等待能够获取它的线程队列.昂贵的方面是等待队列.将线程放入队列,将其从线程调度中删除,并最终在当前所有者释放监视器时将其唤醒,这些操作可能需要很长时间.

但是,在非竞争情况下,等待队列当然不涉及.获取监视器由单个组成,CAS将状态从"无主"(通常为零)更改为"拥有,获取一次"(猜测典型值).如果成功,则线程可以继续执行关键操作,然后是释放,这意味着只需编写具有必要内存可见性的"无主"状态并唤醒另一个被阻塞的线程(如果有).

由于等待队列的成本要高得多,因此实现通常会尝试通过执行一定量的旋转来避免它,即使在竞争情况下也是如此,CAS在重新排入线程之前进行多次重复尝试.如果所有者的关键动作与单个乘法一样简单,则在旋转阶段已经释放监视器的可能性很高.注意这synchronized是"不公平的",允许旋转线程立即进行,即使已经排队的线程等待的时间更长.

如果你比较synchronized(lock){ n = n * 123; }没有排队时所执行的基本操作al.updateAndGet(x -> x * 123);,你会发现它们大致相同.主要区别在于该AtomicLong方法将重复争用的乘法,而对于该synchronized方法,如果在旋转期间没有进展,则存在被放入队列的风险.

但是synchronized允许代码在同一对象上重复同步的锁粗化,这可能与调用该syncShared方法的基准循环相关.除非还有一种融合a的多个CAS更新的方法,否则AtomicLong这可以带来synchronized显着的优势.(另见本文涉及上述几个方面)

请注意,由于"不公平"的性质synchronized,创建比CPU核心更多的线程不一定是个问题.在最好的情况下,"线程数减去核心数"线程最终在队列中,从不唤醒,而其余线程在旋转阶段成功,每个核心上有一个线程.但同样,没有在CPU内核上运行的线程也不能降低AtomicLong更新的性能,因为它们既不能使当前值无效,也不会使CAS尝试失败.

在任何一种情况下,当CAS在非共享对象的成员变量上或在非共享对象上执行synchronized时,JVM可以检测操作的本地性质并且消除大部分相关成本.但这可能取决于几个微妙的环境方面.


底线是原子更新和synchronized块之间没有简单的决定.对于更昂贵的操作,事情变得更加有趣,这可能会增加线程在竞争情况下入队的可能性synchronized,这可能使得在原子更新的竞争情况下必须重复操作是可接受的.


Mik*_*bel 5

您应该阅读,重新阅读并接受@Holger的出色答案,因为它提供的见解远比来自一个开发人员工作站的一组基准数字更有价值。

我对基准进行了调整,使它们之间的关系更加明显,但是如果您阅读@Holger的答案,您会明白为什么这不是一个非常有用的测试。我将包括所做的更改和结果,以简单地说明一台计算机(或一个JRE版本)到另一台计算机的结果如何变化。

首先,我的基准测试版本:

@State(Scope.Benchmark)
public class SandBox {
    public static void main(String[] args) throws RunnerException {
        new Runner(
            new OptionsBuilder().include(SandBox.class.getSimpleName())
                                .shouldFailOnError(true)
                                .mode(Mode.AverageTime)
                                .timeUnit(TimeUnit.NANOSECONDS)
                                .warmupIterations(5)
                                .warmupTime(TimeValue.seconds(5))
                                .measurementIterations(5)
                                .measurementTime(TimeValue.seconds(5))
                                .threads(-1)
                                .build()
        ).run();
    }

    private long number = 0xCAFEBABECAFED00DL;
    private final Object lock = new Object();
    private final AtomicLong atomicNumber = new AtomicLong(number);

    @Setup(Level.Iteration)
    public void setUp() {
        number = 0xCAFEBABECAFED00DL;
        atomicNumber.set(number);
    }

    @Fork(1)
    @Benchmark
    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    public long casShared() {
        return atomicNumber.updateAndGet(x -> x * 123L);
    }

    @Fork(1)
    @Benchmark
    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    public long syncShared() {
        synchronized (lock) {
            return number *= 123L;
        }
    }

    @Fork(value = 1, jvmArgsAppend = "-XX:-UseBiasedLocking")
    @Benchmark
    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    public long syncSharedNonBiased() {
        synchronized (lock) {
            return number *= 123L;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后是我的第一批结果:

@State(Scope.Benchmark)
public class SandBox {
    public static void main(String[] args) throws RunnerException {
        new Runner(
            new OptionsBuilder().include(SandBox.class.getSimpleName())
                                .shouldFailOnError(true)
                                .mode(Mode.AverageTime)
                                .timeUnit(TimeUnit.NANOSECONDS)
                                .warmupIterations(5)
                                .warmupTime(TimeValue.seconds(5))
                                .measurementIterations(5)
                                .measurementTime(TimeValue.seconds(5))
                                .threads(-1)
                                .build()
        ).run();
    }

    private long number = 0xCAFEBABECAFED00DL;
    private final Object lock = new Object();
    private final AtomicLong atomicNumber = new AtomicLong(number);

    @Setup(Level.Iteration)
    public void setUp() {
        number = 0xCAFEBABECAFED00DL;
        atomicNumber.set(number);
    }

    @Fork(1)
    @Benchmark
    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    public long casShared() {
        return atomicNumber.updateAndGet(x -> x * 123L);
    }

    @Fork(1)
    @Benchmark
    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    public long syncShared() {
        synchronized (lock) {
            return number *= 123L;
        }
    }

    @Fork(value = 1, jvmArgsAppend = "-XX:-UseBiasedLocking")
    @Benchmark
    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    public long syncSharedNonBiased() {
        synchronized (lock) {
            return number *= 123L;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

回想一下,您曾synchronized在激烈的竞争中脱颖而出。在我的工作站上,原子版本的性能更好。如果使用我的基准测试版本,您会看到什么结果?如果它们有本质上的不同,那至少不会令我感到惊讶。

这是在一个月前发布的Java 9 EA版本中运行的另一组代码:

# VM version: JDK 1.8.0_60, VM 25.60-b23

Benchmark                    Mode  Cnt     Score     Error  Units
SandBox.casShared            avgt    5   976.215 ± 167.865  ns/op
SandBox.syncShared           avgt    5  1820.554 ±  91.883  ns/op
SandBox.syncSharedNonBiased  avgt    5  1996.305 ± 124.681  ns/op
Run Code Online (Sandbox Code Playgroud)

这里的区别不太明显。在主要的JRE版本之间看到差异并不罕见,但是谁又说您在次要发行版中也不会看到它们呢?

最终,结果将很接近。很接近。synchronized自早期的Java版本以来,的性能已经走了很长一段路。如果您不是在编写HFT算法或其他对时延非常敏感的算法,则应该选择最容易证明正确的解决方案。通常synchronized比无锁算法和数据结构更容易推理。如果您无法在应用程序中证明可测量的差异,那么synchronized应该使用。