在gradle工程使用jmh并集成SpringBoot

什么是JMH

JMH,Java Microbenchmark Harness,是专门用于代码微基准测试的工具套件。

jmh官网

官方demo

我们为什么需要JMH

大家可能会疑问,我就用这样的方式来测试效率不好吗?

long start=System.currentTimeMillis();
doxxx();
Systime.out.println(System.currentTimeMillis()-start);

这么说来原因就很多了,比如:

  1. 程序预热(线程池是否已经扩容完毕、JIT热点代码)
  2. 程序一般要运行多次才能计算出比较准确的耗时
  3. 前置逻辑中产生的对象导致gc,结果影响当前方法的测试
  4. 多线程并行、并发场景下的测试

gradle依赖

testImplementation 'org.openjdk.jmh:jmh-core:1.33'
testAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.33'

String效率

关于字符串拼接的效率问题也算是老生常谈了,都说如果要循环拼接字符串,那最好使用StringBuilder,不要直接用+。但是很少人能提供明确的数据支撑。接下来我们先看个demo,感受一下jmh。

package com.example.jmh;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(time = 1, iterations = 5)
@Measurement(time = 1, iterations = 5)
@Fork(1)
@State(value = Scope.Benchmark)
public class StringTest {

    @Param({"10", "100", "1000"})
    private int size;

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(StringTest.class.getSimpleName())
                .result("StringTest_result.json")
                .resultFormat(ResultFormatType.JSON).build();
        new Runner(opt).run();
    }

    @Benchmark
    public String testAdd() {
        String a = "";
        for (int i = 0; i < size; i++) {
            a = a + i;
        }
        return a;
    }

    @Benchmark
    public String testConcat() {
        String a = "";
        for (int i = 0; i < size; i++) {
            a = a.concat("" + i);
        }
        return a;
    }

    @Benchmark
    public String testBuilder() {
        StringBuilder a = new StringBuilder();
        for (int i = 0; i < size; i++) {
            a.append(i);
        }
        return a.toString();
    }

    @Benchmark
    public String testBuffer() {
        StringBuffer a = new StringBuffer();
        for (int i = 0; i < size; i++) {
            a.append(i);
        }
        return a.toString();
    }
}

结果

Benchmark               (size)  Mode  Cnt       Score       Error  Units
StringTest.testAdd          10  avgt    5     127.439 ±     3.774  ns/op
StringTest.testAdd         100  avgt    5    1915.880 ±    41.530  ns/op
StringTest.testAdd        1000  avgt    5  199442.392 ± 21708.330  ns/op
StringTest.testBuffer       10  avgt    5      89.245 ±     1.318  ns/op
StringTest.testBuffer      100  avgt    5    1026.880 ±    10.613  ns/op
StringTest.testBuffer     1000  avgt    5   13192.122 ±   174.511  ns/op
StringTest.testBuilder      10  avgt    5      71.272 ±     2.464  ns/op
StringTest.testBuilder     100  avgt    5     870.357 ±    14.289  ns/op
StringTest.testBuilder    1000  avgt    5   11498.802 ±   253.298  ns/op
StringTest.testConcat       10  avgt    5     312.639 ±    10.854  ns/op
StringTest.testConcat      100  avgt    5    3675.755 ±   163.944  ns/op
StringTest.testConcat     1000  avgt    5  204830.905 ±  3241.692  ns/op

从上面的结果中可以看到,随着拼接次数的增加10->100->1000每个方法的效率都会降低。但是使用StringBuilder进行字符串拼接的效率依旧是最高的,其次是StringBuffer,然后是字符串加法,效率最低的concat。

基本参数概念

@BenchmarkMode

标识JMH进行Benchmark时所使用的模式。

  1. Throughput:吞吐量。比如“1秒内可以执行多少次调用”,单位是ops/time
  2. AverageTime:每次调用的平均耗时。比如“每次调用平均耗时xxx毫秒”,单位是time/ops
  3. SampleTime:随机取样,最后输出取样结果的分布。比如“99%的调用在xxx毫秒内,99.99%的调用在xxx毫秒以内”
  4. SingleShotTime:只运行一次,往往同时设置warmup=0,一般用于测试冷启动的性能。

上面的这些模式并不是只能使用某一个,这些模式是可以被组合使用的,比如

@BenchmarkMode({Mode.AverageTime, Mode.SampleTime})

@State

通过State可以指定一个对象的作用范围,jmh通过scope来进行实例化和共享操作。@State可以被继承使用,如果父类定义了该注解,子类则无需定义。由于jmh可以进行多线程测试,所以不同的scope的隔离级别如下:

  1. Scope.Benchmark:全局共享,所有的测试线程共享同一个实例对象。可以用来测试有状态的实例在多线程下的性能。
  2. Scope.Group:同一个线程组内部的线程共享一个实例对象。
  3. Scope.Thread:每个线程获取到都是不一样的实例对象。

在上面字符串拼接性能测试的样例中,我们使用的就是Scope.Benchmark

@OutputTimeunit

输出结果的时间单位,咱们上面用的是纳秒,即TimeUnit.NANOSECONDS

@Warmup

程序预热所需要的一些参数,可以用在类或者方法上。由于JVM存在JIT机制,所以一般前几次的效率都可能会不太理想,所以需要让程序先预热一下再跑。这样可以保证测试结果的准确性。参数如下:

  1. iterations:预热的次数,默认值是org.openjdk.jmh.runner.Defaults#WARMUP_ITERATIONS=5
  2. time:每次预热执行的时长,默认值是org.openjdk.jmh.runner.Defaults#WARMUP_TIME=10秒
  3. timeUnit:上面那个时长对应的单位类型,默认是秒
  4. batchSize:每个操作的基准方法调用次数,默认值是org.openjdk.jmh.runner.Defaults#WARMUP_BATCHSIZE=1。1就代表一次一次的调用,如果是2那就代表2次2次的调用。

@Measurement

这个参数与@Warmup中的参数完全一样,只是@Warmup是用在预热上,预热结果不算入最终的结果中。而@Measurement是指实际测试时的参数。

@Fork

默认值是org.openjdk.jmh.runner.Defaults#MEASUREMENT_FORKS=5,可以手动指定。@Fork
中设置是多少,那jmh执行测试时就会创建多少个独立的进程来进行测试。但是需要注意的是,不管有多少个进程进行测试,他们都是串行的。当fork为0时,表示不需要进行fork。官方解释是这样的:

JVMs are notoriously good at profile-guided optimizations. This is bad for benchmarks, because different tests can mix their profiles together, and then render the “uniformly bad” code for every test. Forking (running in a separate process) each test can help to evade this issue.
JMH will fork the tests by default.

翻译成人话就是说,首先因为JVM存在profile-guided optimizations
的特性,但是这样的特性是不利于进行基准测试的。因为不同的测试方法会混在一起,最后会导致结果出现偏差。为了避免这样的偏差才有了@Fork的存在。关于这个偏差的问题可以参考官方的这个例子:code-tools/jmh: 2be2df7dbaf8 jmh-samples/src/main/java/org/openjdk/jmh/samples/JMHSample_12_Forking.java

所以为了避免这样的问题,我们可以设置@Fork(1)这样每一个测试的方法都会跑在不同的jvm进程中,也就避免了相互影响。

@Thread

每一个测试进程(JVM)中的线程数。

@Param

用来指定某个参数的多种情况,比如上面字符串的例子中的:

@Param({"10", "100", "1000"})
private int size;

特别适合用来测试一个函数在不同的参数输入的情况下的性能。只能用在字段上,同时必须使用@State注解声明隔离级别。

实战

一、Random与ThreadLocalRandom

我们都知道获取随机数可以使用Random,同时在官方文档中也强调Random虽然是线程安全的,但是如果在多线程的情况下,最好还是使用ThreadLocalRandom
。那么,Random与ThreadLocalRandom在效率上相差多少呢?我们在实际使用过程中该如何选择呢?

@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(time = 1, iterations = 3)
@Measurement(time = 1, iterations = 5)
@Fork(1)
@Threads(5)
@State(value = Scope.Benchmark)
public class RandomTest {
    private final Random random = new Random();
    private final ThreadLocal<Random> randomThreadLocalHolder = ThreadLocal.withInitial(Random::new);

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(RandomTest.class.getSimpleName())
                .result("RandomTest_result.json")
                .resultFormat(ResultFormatType.JSON).build();
        new Runner(opt).run();
    }

    @Benchmark
    public int random() {
        return random.nextInt();
    }

    @Benchmark
    public int randomThreadLocalHolder() {
        return randomThreadLocalHolder.get().nextInt();
    }

    @Benchmark
    public int threadLocalRandom() {
        return ThreadLocalRandom.current().nextInt();
    }
}

看下结果:

Benchmark Mode Cnt Score Error Units
RandomTest.random avgt 5 423.784 ± 20.159ns/op
RandomTest.randomThreadLocalHolder avgt 5 11.369 ±  0.509ns/op
RandomTest.threadLocalRandom avgt 5 4.322 ±  0.374ns/op

从结果上看ThreadLocalRandom.current().nextInt()
完胜,而且效率差别非常大。同时我们也没必要自己搞ThreadLocal来封装Random。因为JDK提供的ThreadLocalRandom.current()就已经是天花板了。

二、写热点

@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(time = 1, iterations = 3)
@Measurement(time = 1, iterations = 5)
@Fork(1)
@Threads(10)
@State(value = Scope.Benchmark)
public class HotWriteTest {
    private final LongAdder longAdder = new LongAdder();
    private final AtomicLong atomicLong = new AtomicLong();

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(HotWriteTest.class.getSimpleName())
                .result("HotWriteTest_result.json")
                .resultFormat(ResultFormatType.JSON).build();
        new Runner(opt).run();
    }

    @Benchmark
    public void longAdder() {
        longAdder.increment();
    }

    @Benchmark
    public void atomicLong() {
        atomicLong.incrementAndGet();
    }
}

测试结果:

Benchmark                Mode  Cnt    Score    Error  Units
HotWriteTest.atomicLong  avgt    5  210.160 ± 27.965  ns/op
HotWriteTest.longAdder   avgt    5   14.293 ±  2.339  ns/op

三、同步队列性能测试+SpringBoot集成

SpringBoot工程如下:

public interface IQueue {
    void put(Object o) throws InterruptedException;

    Object take() throws InterruptedException;
}

@Component("arrayQueue")
public class ArrayQueue implements IQueue {
    private static final ArrayBlockingQueue<Object> QUEUE = new ArrayBlockingQueue<>(100000);

    @Override
    public void put(Object o) throws InterruptedException {
        QUEUE.put(o);
    }

    @Override
    public Object take() throws InterruptedException {
        return QUEUE.take();
    }
}

@Component("linkedQueue")
public class LinkedQueue implements IQueue {
    private static final LinkedBlockingQueue<Object> QUEUE = new LinkedBlockingQueue<>(100000);

    @Override
    public void put(Object o) throws InterruptedException {
        QUEUE.put(o);
    }

    @Override
    public Object take() throws InterruptedException {
        return QUEUE.take();
    }
}

@SpringBootApplication(scanBasePackages = "com.example.jmh")
public class SpringBootApp {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringBootApp.class, args);
        IQueue arrayQueue = context.getBean("arrayQueue", IQueue.class);
        IQueue linkedQueue = context.getBean("linkedQueue", IQueue.class);
    }
}

测试代码如下:

@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(time = 1, iterations = 2)
@Measurement(time = 1, iterations = 3)
@Fork(1)
@State(Scope.Group)
public class SpringBootTest {

    private ConfigurableApplicationContext applicationContext;
    private IQueue arrayQueue;
    private IQueue linkedQueue;

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(SpringBootTest.class.getSimpleName())
                .result("SpringBootTest.json")
                .resultFormat(ResultFormatType.JSON).build();
        new Runner(opt).run();
    }

    @Setup
    public void init() {
        applicationContext = SpringApplication.run(SpringBootApp.class);
        arrayQueue = applicationContext.getBean("arrayQueue", IQueue.class);
        linkedQueue = applicationContext.getBean("linkedQueue", IQueue.class);
    }

    @TearDown
    public void down() {
        applicationContext.close();
    }

    @Group("arrayQueue")
    @GroupThreads(2)
    @Benchmark
    public void arrayQueuePut() throws InterruptedException {
        arrayQueue.put(new Object());
    }

    @Group("arrayQueue")
    @GroupThreads(10)
    @Benchmark
    public Object arrayQueueGet() throws InterruptedException {
        return arrayQueue.take();
    }

    @Group("linkedQueue")
    @GroupThreads(2)
    @Benchmark
    public void linkedQueuePut() throws InterruptedException {
        linkedQueue.put(new Object());
    }

    @Group("linkedQueue")
    @GroupThreads(10)
    @Benchmark
    public Object linkedQueueGet() throws InterruptedException {
        return linkedQueue.take();
    }
}

测试结果:

Benchmark                                  Mode  Cnt     Score     Error  Units
SpringBootTest.arrayQueue                  avgt    3   719.003 ± 139.696  ns/op
SpringBootTest.arrayQueue:arrayQueueGet    avgt    3   829.722 ± 162.348  ns/op
SpringBootTest.arrayQueue:arrayQueuePut    avgt    3   165.408 ±  26.638  ns/op
SpringBootTest.linkedQueue                 avgt    3  1019.508 ±  95.074  ns/op
SpringBootTest.linkedQueue:linkedQueueGet  avgt    3  1176.427 ± 109.428  ns/op
SpringBootTest.linkedQueue:linkedQueuePut  avgt    3   234.914 ±  23.304  ns/op

与JUnit的区别

俗话说条条大路通罗马。JUnit解决是测试一条道路能不能通往罗马,而JMH是测试哪条通往罗马的道路最快。在我看来,JUnit更多的是功能测试,而JMH是性能测试。这两个测试的不是一个方面。

参考链接

为什么要用JMH?何时应该用? - 知乎 (zhihu.com)

JMH(Java Micro Benchmark) 简介 - 逝者如斯夫 - BlogJava

JMH使用说明 - 简书 (jianshu.com)

JMH使用说明_lxbjkben的博客-CSDN博客_jmh使用

JAVA 拾遗 — JMH 与 8 个测试陷阱 - 徐靖峰|个人博客 (cnkirito.moe)

JMH 应用指南 | JAVATECH (dunwu.github.io)

jmh学习笔记-Forking分叉_m0_37607945的博客-CSDN博客

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值