【发布时间】: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 个线程上执行。 结果让我吃惊:
- CAS 在 1 个线程中最有效。 2 线程 - 结果与 监视器。 3 次或更多 - 比监视器差,~ 2 次。
- 在大多数情况下,自定义实现比监视器好 2-3 倍。
- 但在自定义实现中,随机有时会发生错误执行。好的情况 - 50 op/微秒。坏情况 - 0.5 op/微秒。
问题:
- 为什么 AtomicInteger 不基于同步,它比当前的 impl 更有生产力?
- 为什么 AtomicInteger 在 CAS 失败时不使用 LockSupport.parkNanos(1)?
- 为什么在自定义实现中会出现这种峰值?
我尝试执行此测试几次,但峰值总是发生在不同数量的线程中。我也在另一台机器上试过这个测试,结果是一样的。可能是测试中的问题。在 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 result graphs. X - threads count, Y - thpt, op/microsec
更新
好的,我知道,我明白了,当我使用 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你检查你的日志文件了吗?有很多<failure: VM prematurely exited before JMH had finished with it, explicit System.exit was called?>事件 -
如果您查看代码,您将在 @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过早退出时,您将没有结果。