Java微基准测试框架JMH中AverageTime与SingleShotTime的区别及配置疑问
JMH SingleShotTime模式行为疑问
已理解的JMH模式区别
我已明确JMH中AverageTime与Throughput模式的差异:
假设基准方法带有如下注解:
@Benchmark @Fork(value = 1, warmups = 0) @Measurement(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)
- 使用
@BenchmarkMode(Mode.AverageTime)时:JMH启动1个fork,执行1次迭代,在至少1秒内尽可能多次调用基准方法(即Level.Invocation级别),最终通过总耗时÷调用次数得到单次调用的平均时间。 - 使用
@BenchmarkMode(Mode.Throughput)时:逻辑类似,但最终通过完成调用次数÷总耗时计算吞吐量。
问题场景:SingleShotTime模式的不符合预期行为
我编写了一个带状态的基准测试类,预期SingleShotTime模式下仅调用一次基准方法并测量其耗时,但实际出现了多次调用,且多线程共用状态实例导致冲突:
@State(Scope.Benchmark) internal open class FooBenchmark { class Foo { private val values = mutableListOf<String>() fun add() = values.add(values.size.toString()) fun size() = values.size } private val foo = Foo() @Benchmark @Fork(value = 1, warmups = 0) @Measurement(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) @BenchmarkMode(Mode.SingleShotTime) fun fooBenchmark(hole: Blackhole) { check(foo.size() == 0) foo.add() check(foo.size() == 1) hole.consume(foo) } }
运行基准测试时,JMH输出如下:
> Task :lib:runBenchmark # Detecting actual CPU count: 8 detected # JMH version: 1.37 # VM version: JDK 19.0.1, OpenJDK 64-Bit Server VM, 19.0.1+10-21 # VM invoker: /opt/jdk-19.0.1/bin/java # VM options: -Dfile.encoding=UTF-8 -Duser.country=US -Duser.language=en -Duser.variant # Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable) # Warmup: <none> # Measurement: 1 iterations, 1 s each # Timeout: 10000 ms per iteration # Threads: 8 threads # Benchmark mode: Single shot invocation time # Benchmark: org.kepocnhh.jmh.FooBenchmark.fooBenchmark
实际运行中,JMH会多次调用fooBenchmark方法,且8个线程共用同一个FooBenchmark实例(因Scope.Benchmark),导致foo状态的并发访问冲突,触发check断言失败。我不确定是对模式原理理解有误,还是配置存在问题。
问题原因与解决方法
核心原因
- 默认线程数导致多线程调用:JMH默认线程数等于CPU核心数(输出中显示
8 threads),每个线程都会独立执行基准方法,这直接导致多次调用。 - 测量时间配置导致持续调用:
@Measurement(time = 1)表示迭代会持续1秒,在这段时间内,每个线程会尽可能多次调用基准方法——SingleShotTime模式的核心是测量单次调用的耗时,但它仍然会在配置的迭代时间/次数内重复调用以获取统计样本,而非只调用一次。
实现“仅调用一次基准方法”的正确配置
要达到“仅调用一次基准方法并测量耗时”的需求,需调整两个配置:
- 指定线程数为1:添加
@Threads(1)注解,避免多线程并发调用 - 调整测量方式为“调用次数”而非“持续时间”:将
@Measurement的time参数替换为count = 1,表示每次迭代仅调用1次基准方法
修改后的基准方法注解示例:
@Benchmark @Fork(value = 1, warmups = 0) @Measurement(iterations = 1, count = 1) // 每次迭代仅调用1次 @Threads(1) @BenchmarkMode(Mode.SingleShotTime) fun fooBenchmark(hole: Blackhole) { check(foo.size() == 0) foo.add() check(foo.size() == 1) hole.consume(foo) }
补充说明
Scope.Benchmark表示整个基准测试共享一个状态实例,若不需要多线程共享,可改为Scope.Thread,每个线程拥有独立的状态实例,避免并发冲突;但如果只是要单次调用,@Threads(1)已经足够。SingleShotTime适合测量单次操作的绝对耗时,比如冷启动场景,此时搭配count=1和Threads(1)才能实现“单次调用测量”的预期。
内容的提问来源于stack exchange,提问作者Stanley Wintergreen
相关产品推荐
相关产品推荐

