JMH是Java最可靠的微基准测试工具,需规范配置预热、fork、JVM参数及避免副作用;功能级压测宜用线程池+Future自主计时;JMeter易受外部因素干扰,应结合jstack/jstat和代码打点定位瓶颈。

怎么用 JMH 写出靠谱的并发基准测试
直接手写 Thread 启动一百个任务再算耗时,结果基本不可信——JVM 预热没做、GC 干扰没控、测量方式不原子,测出来的数字连趋势都反映不了。JMH 是目前 Java 生态最可靠的微基准测试工具,它强制你面对预热、fork、统计方式这些关键控制点。
关键实操建议:
- 必须用
@Fork(jvmArgsAppend = {"-Xmx2g", "-XX:+UseG1GC"})显式指定 JVM 参数,否则默认 fork 的 JVM 可能用 CMS 或小堆,干扰吞吐量对比 -
@State(Scope.Benchmark)下的共享对象(比如ConcurrentHashMap)要小心:多个线程共用同一实例,但 JMH 会为每个 fork 单独初始化一次,这点比手写测试更可控 - 避免在
@Benchmark方法里做 I/O、日志、随机数生成等副作用操作;如果真要测带锁逻辑,用@Group("lock")+@GroupThreads(4)模拟竞争更真实
@Fork(3)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@State(Scope.Benchmark)
public class ConcurrentHashMapPutBenchmark {
private ConcurrentHashMap map;
@Setup
public void setup() {
map = new ConcurrentHashMapzuojiankuohaophpcnyoujiankuohaophpcn();
}
@Benchmark
public void put(@Param({"1000", "10000"}) int size) {
for (int i = 0; i zuojiankuohaophpcn size; i++) {
map.put(i, "val" + i);
}
}}
ThreadPoolExecutor + CountDownLatch 做功能级压测时的陷阱
当你要验证“接口在 500 QPS 下是否稳定”,而不是测单个方法的纳秒级性能时,JMH 就不合适了。这时候得自己搭轻量压测主干,但多数人栽在资源泄漏和计时偏差上。
立即学习“Java免费学习笔记(深入)”;
常见错误现象:
- 用
System.nanoTime() 包裹整个 executor.invokeAll(tasks) —— 实际测的是任务提交耗时,不是执行耗时
-
CountDownLatch 在异常路径下没被 countDown(),导致主线程永远阻塞
- 线程池
corePoolSize 设太小(比如 2),大量任务排队,ThreadPoolExecutor.getCompletedTaskCount() 返回值远低于预期,并发度根本没打上去
正确做法是每个任务自己记录执行时间,并汇总统计:
ExecutorService executor = Executors.newFixedThreadPool(50);
List> futures = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
futures.add(executor.submit(() -> {
long start = System.nanoTime();
// 调用目标方法,如 service.process()
long end = System.nanoTime();
return end - start;
}));
}
List durations = futures.stream()
.map(future -> {
try { return future.get(); }
catch (Exception e) { return 0L; }
})
.filter(d -> d > 0)
.collect(Collectors.toList());
为什么用 JMeter 测 Java 服务反而容易误判
JMeter 本身是外部工具,它测的是“从发请求到收响应”的端到端延迟,包含网络、序列化、反向代理、甚至客户端 DNS 解析。如果你发现加了 synchronized 后 JMeter 报告的平均 RT 只涨了 2ms,别急着说“锁没影响”,很可能瓶颈根本不在 Java 层。
排查优先级建议:
- 先用
jstack -l 看是否有线程长期 BLOCKED 在锁上;用 jstat -gc 确认是不是 GC 导致 STW 拉高了 P99
- 在目标方法入口加
System.nanoTime() 打点,把日志输出到独立文件,和 JMeter 时间戳对齐,确认 Java 层真实耗时占比
- 如果服务走 HTTP,用
curl -w "@format.txt" -o /dev/null -s http://localhost:8080/api 直连测试,绕过 JMeter 的 GUI 开销和定时器抖动
LockSupport.parkNanos 和自旋等待的实际开销差异
在写无锁算法或高性能队列时,有人倾向用 while(!condition) Thread.onSpinWait(),也有人用 LockSupport.parkNanos(1)。看起来都是“等一下”,但实际对 CPU 和调度的影响天差地别。
关键区别:
-
Thread.onSpinWait() 是纯 CPU 占用,适合等待极短时间(
-
LockSupport.parkNanos(1) 会让线程进入 OS 级等待态,即使纳秒级参数,实际唤醒延迟常达数微秒,且上下文切换成本远高于自旋
- 真正该用 park 的场景是:你明确知道要等 >1μs,且不希望线程持续抢占 CPU;比如 AQS 中
shouldParkAfterFailedAcquire 判定后才 park
一句话判断:只要你的“等”不是在循环里反复执行、且无法预估等待时长,就别碰 onSpinWait —— 它不是银弹,是手术刀。











