【问题标题】:Java Increment benchmark [closed]Java增量基准[关闭]
【发布时间】:2026-02-18 17:25:07
【问题描述】:

我对多线程增量的最佳性能进行了调查。我检查了基于同步、AtomicInteger 和自定义实现(如 AtomicInteger 中)的实现,但使用 parkNanos(1),CAS 失败。

private int customAtomic() {
        int ret;
        for (;;) {
            ret = intValue;
            if (unsafe.compareAndSwapInt(this, offsetIntValue, ret, ++ret)) {
                break;
            }
            LockSupport.parkNanos(1);
        }
        return ret;
    }

我基于JMH做了基准测试:明确执行每个方法,每个方法消耗CPU(1,2,4,8,16次)并且只消耗CPU。每种基准测试方法在 Intel(R) Xeon(R) CPU E5-1680 v2 @ 3.00GHz、8 Core + 8 HT 64Gb RAM、1-17 个线程上执行。 结果让我吃惊:

  1. CAS 在 1 个线程中最有效。 2 线程 - 结果与 监视器。 3 次或更多 - 比监视器差,~ 2 次。
  2. 在大多数情况下,自定义实现比监视器好 2-3 倍。
  3. 但在自定义实现中,随机有时会发生错误执行。好的情况 - 50 op/微秒。坏情况 - 0.5 op/微秒。

问题:

  1. 为什么 AtomicInteger 不基于同步,它比当前的 impl 更有生产力?
  2. 为什么 AtomicInteger 在 CAS 失败时不使用 LockSupport.parkNanos(1)?
  3. 为什么在自定义实现中会出现这种峰值?

我尝试执行此测试几次,但峰值总是发生在不同数量的线程中。我也在另一台机器上试过这个测试,结果是一样的。可能是测试中的问题。在 StackProfiler 中自定义 impl 的“坏情况”中,我看到:

....[Thread state distributions]....................................................................
 50.0%         RUNNABLE
 49.9%         TIMED_WAITING

....[Thread state: RUNNABLE]........................................................................
 43.3%  86.6% sun.misc.Unsafe.park
  5.8%  11.6% com.jad.generated.IncrementBench_incrementCustomAtomicWithWork_jmhTest.incrementCustomAtomicWithWork_thrpt_jmhStub
  0.8%   1.7% org.openjdk.jmh.infra.Blackhole.consumeCPU
  0.1%   0.1% com.jad.IncrementBench$Worker.work
  0.0%   0.0% java.lang.Thread.currentThread
  0.0%   0.0% com.jad.generated.IncrementBench_incrementCustomAtomicWithWork_jmhTest._jmh_tryInit_f_benchmarkparams1_0
  0.0%   0.0% org.openjdk.jmh.infra.generated.BenchmarkParams_jmhType_B1.<init>

....[Thread state: TIMED_WAITING]...................................................................
 49.9% 100.0% sun.misc.Unsafe.park

在“好的情况下”:

....[Thread state distributions]....................................................................
 88.2%         TIMED_WAITING
 11.8%         RUNNABLE

....[Thread state: TIMED_WAITING]...................................................................
 88.2% 100.0% sun.misc.Unsafe.park

....[Thread state: RUNNABLE]........................................................................
  5.6%  47.9% sun.misc.Unsafe.park
  3.1%  26.3% org.openjdk.jmh.infra.Blackhole.consumeCPU
  2.4%  20.3% com.jad.generated.IncrementBench_incrementCustomAtomicWithWork_jmhTest.incrementCustomAtomicWithWork_thrpt_jmhStub
  0.6%   5.5% com.jad.IncrementBench$Worker.work
  0.0%   0.0% com.jad.generated.IncrementBench_incrementCustomAtomicWithWork_jmhTest.incrementCustomAtomicWithWork_Throughput
  0.0%   0.0% java.lang.Thread.currentThread
  0.0%   0.0% org.openjdk.jmh.infra.generated.BenchmarkParams_jmhType_B1.<init>
  0.0%   0.0% sun.misc.Unsafe.putObject
  0.0%   0.0% org.openjdk.jmh.runner.InfraControlL2.announceWarmdownReady
  0.0%   0.0% sun.misc.Unsafe.compareAndSwapInt

Link to benchmark code

Link to result graphs. X - threads count, Y - thpt, op/microsec

Link to RAW log

更新

好的,我知道,我明白了,当我使用 parkNanos 时,一个线程也可以长时间持有锁(CAS)。 CAS 失败的线程进入睡眠状态,只有一个线程在工作并增加值。我明白了,对于大并发级别,当工作如此之少时 - AtomicInteger 不是更好的方法。但是如果我们增加 workSize,例如 level = CASThrpt/threadNum,它应该可以正常工作: 对于本地机器,我设置了 workSize=300,我的测试结果:

Benchmark                                     (workSize)   Mode  Cnt  Score   Error   Units
IncrementBench.incrementAtomicWithWork               300  thrpt    3  4.133 ± 0.516  ops/us
IncrementBench.incrementCustomAtomicWithWork         300  thrpt    3  1.883 ± 0.234  ops/us
IncrementBench.lockIntWithWork                       300  thrpt    3  3.831 ± 0.501  ops/us
IncrementBench.onlyWithWork                          300  thrpt    3  4.339 ± 0.243  ops/us

AtomicInteger - 获胜,锁定 - 第二名,自定义 - 第三名。 但是尖峰的问题,仍然不清楚。我忘记了java版本: Java(TM) SE 运行时环境 (build 1.7.0_79-b15) Java HotSpot(TM) 64 位服务器 VM(内部版本 24.79-b02,混合模式)

【问题讨论】:

  • 移除对 parkNanos 的调用。您想尽可能快地再次迭代。还要确保 intValue 是易变的。否则 ret=intValue 可能看不到与 CAS 相同的值
  • But problem with spikes, still not clear 你检查你的日志文件了吗?有很多&lt;failure: VM prematurely exited before JMH had finished with it, explicit System.exit was called?&gt;事件
  • 如果您查看代码,您将在 @Setup 方法中看到 System.exit(0)。它用于删除无意义的情况,例如:使用参数 workSize (2,4,8..) 清除 AtomicInteger 增量。这种情况与参数无关。
  • 好的,使用 3 个线程和 workSize = 1 测试 incrementCustomAtomicWithWork 由于 failure: VM prematurely exited 而失败,结果为 0.301 ops/us。没事吧?
  • 不,它与之前的测试增量CustomAtomic,workSize = 16有关。当VM过早退出时,您将没有结果。

标签: java increment atomic jmh


【解决方案1】:

在同步的情况下,它往往会粘住锁,这意味着一个线程可以长时间持有锁,而不会让另一个线程公平地抓住它。这对于多线程来说是非常糟糕的,但如果你有一个基准测试,如果只有一个线程运行相对较长的时间,它会表现得更好。

您需要更改测试,使其在使用多个线程时比仅使用一个线程运行得更好,否则您实际上将测试哪种锁定策略的公平性策略最差。

锁定策略试图调整锁定的执行方式,这就是它可以改变行为的原因,但它不能很好地工作,因为代码本来就不应该是多线程的。

【讨论】: