JMH用法详情(实例测试对比单线程和多线程的区别)

一、JMH是什么?

JMH是什么?
JMH是Java性能测试工具,主要是对工程中一些方法进行一些基准测试,支持的时间单位为:nano / micro / milli / macro

二、JMH的Jar包

Java项目开始编写JMH实例时肯定要先知道引用的哪些Jar包,JMH使用引用Jar包如下:

  <dependency>
      <groupId>org.openjdk.jmh</groupId>
      <artifactId>jmh-core</artifactId>
      <version>1.19</version>
    </dependency>
    <dependency>
      <groupId>org.openjdk.jmh</groupId>
      <artifactId>jmh-generator-annprocess</artifactId>
      <version>1.19</version>
      <scope>provided</scope>
    </dependency>
  --------------------------------------------------------------------------------------------------------  
    <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-shade-plugin</artifactId>
          <version>1.4</version>
          <executions>
            <execution>
              <phase>package</phase>
              <goals>
                <goal>shade</goal>
              </goals>
              <configuration>
                <finalName>microbenchmarks</finalName>
                <transformers>
                  <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                    <mainClass>org.openjdk.jmh.Main</mainClass>
                  </transformer>
                </transformers>
              </configuration>
            </execution>
          </executions>
        </plugin>
      </plugins>

三、JMH的注解解释

@BenchmarkMode
基准测试类型。这里选择的是Throughput也就是吞吐量。根据源码点进去,每种类型后面都有对应的解释,比较好理解,吞吐量会得到单位时间内可以进行的操作数。

Throughput: 整体吞吐量,例如“1秒内可以执行多少次调用”。
AverageTime: 调用的平均时间,例如“每次调用平均耗时xxx毫秒”。
SampleTime: 随机取样,最后输出取样结果的分布,例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
SingleShotTime: 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。
All(“all”, “All benchmark modes”);
@Warmup
上面我们提到了,进行基准测试前需要进行预热。一般我们前几次进行程序测试的时候都会比较慢, 所以要让程序进行几轮预热,保证测试的准确性。其中的参数iterations也就非常好理解了,就是预热轮数。

为什么需要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。

iterations:预热的次数。
time:每次预热的时间。
timeUnit:时间的单位,默认秒。
batchSize:批处理大小,每次操作调用几次方法。
@Measurement
度量,其实就是一些基本的测试参数。

iterations 进行测试的轮次
time 每轮进行的时长
timeUnit 时长单位
都是一些基本的参数,可以根据具体情况调整。一般比较重的东西可以进行大量的测试,放到服务器上运行。

@Threads
每个进程中的测试线程,可用于类或者方法上。一般选择为cpu乘以2。如果配置了 Threads.MAX ,代表使用 Runtime.getRuntime().availableProcessors() 个线程。

@Fork
进行 fork 的次数。可用于类或者方法上。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。

@OutputTimeUnit
这个比较简单了,基准测试结果的时间类型。一般选择秒、毫秒、微秒。

@Benchmark
方法级注解,表示该方法是需要进行 benchmark 的对象,用法和 JUnit 的 @Test 类似。

@Param
属性级注解,@Param 可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。

@Setup
方法级注解,这个注解的作用就是我们需要在测试之前进行一些准备工作,比如对一些数据的初始化之类的。

@TearDown
方法级注解,这个注解的作用就是我们需要在测试之后进行一些结束工作,比如关闭线程池,数据库连接等的,主要用于资源的回收等。

@Setup主要实现测试前的初始化工作,只能作用在方法上。用法和Junit一样。使用该注解必须定义 @State注解。

@TearDown主要实现测试完成后的垃圾回收等工作,只能作用在方法上。用法和Junit一样。使用该注解必须定义 @State 注解。

这两个注解都有一个 Level 的枚举value,它有三个值(默认的是Trial):

Trial:在每次Benchmark的之前/之后执行。
Iteration:在每次Benchmark的iteration的之前/之后执行。
Invocation:每次调用Benchmark标记的方法之前/之后都会执行。
可见,Level的粒度从Trial到Invocation越来越细。

@State
当使用@Setup参数的时候,必须在类上加这个参数,不然会提示无法运行。

State 用于声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围。 因为很多 benchmark 会需要一些表示状态的类,JMH 允许你把这些类以依赖注入的方式注入到 benchmark 函数里。Scope 主要分为三种。

Thread: 该状态为每个线程独享。
Group: 该状态为同一个组里面所有线程共享。
Benchmark: 该状态在所有线程间共享。

四、开始实例对比单线程和多线程(代码实例)

这个例子是:一个1亿大小的 int 类型数组 里面全存的都是1,
多线程模式下:将这个数组分为四段,用四个线程去进行累加,最后再将每个线程的和相加取得和值
单线程模式下: 直接去遍历从0到1亿进行累加
最后比较单线程和多线程所执行的时间
代码如下:

/**
 * JMH测试单线程和多线程使用的时间
 * */
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3)
@Measurement(iterations = 10)
public class MyBenchmark {
    static int[] ARRAY = new int[1000_000_00];
    static {
        Arrays.fill(ARRAY,1);
    }

    @Benchmark
    public int c() throws ExecutionException, InterruptedException {
        int[] array = ARRAY;
        FutureTask<Integer> t1 = new FutureTask<>(() ->{
            int sum = 0;
            for (int i=0; i<25000000 ; i++) {
                sum += array[0+i];
            }
            return sum;
        });
        FutureTask<Integer> t2 = new FutureTask<>(() -> {
            int sum = 0;
            for (int i=25000000; i<50000000;i++){
                sum += array[0+i];
            }
            return sum;
        });
        FutureTask<Integer> t3 = new FutureTask<>(() -> {
            int sum = 0;
            for (int i=50000000; i<75000000;i++){
                sum += array[0+i];
            }
            return sum;
        });
        FutureTask<Integer> t4 = new FutureTask<>(() -> {
            int sum = 0;
            for (int i=75000000; i<100000000;i++){
                sum += array[0+i];
            }
            return sum;
        });
        new Thread(t1).start();
        new Thread(t2).start();
        new Thread(t3).start();
        new Thread(t4).start();
        return t1.get() + t2.get() + t3.get() + t4.get();
    }

    @Benchmark
    public int d() throws ExecutionException, InterruptedException {
        int[] array = ARRAY;
        FutureTask<Integer> t1 = new FutureTask<>(() ->{
            int sum = 0;
            for (int i=0; i<100000000 ; i++) {
                sum += array[0+i];
            }
            return sum;
        });
        new Thread(t1).start();
        return t1.get();
    }


    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(MyBenchmark.class.getSimpleName())
                .build();
        new Runner(opt).run();
    }

}

五、运行结果

在这里插入图片描述

上面结果
c方法是多线程
d方法是单线程
一目了然可得知在多核CPU下,多线程的运行速度,比单线程快
但是在单核CPU下情况不是很确定,因为多线程有上下文切换,所有可以使用虚拟机去进行测试

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我可以给你一个简单的示例来比较并行流和串行流在 jmh 测试中的性能差异。 假设我们有一个包含 1 百万个整数的列表,并且我们要计算这些数的总和。我们可以使用 Java 8 中的 Stream API 来实现这个任务,而且可以选择使用并行流或串行流来执行计算。 首先,我们来看一下串行流的实现代码: ```java public class SerialStreamBenchmark { private List<Integer> numbers; @Setup public void setup() { numbers = new ArrayList<>(); for (int i = 0; i < 1_000_000; i++) { numbers.add(i); } } @Benchmark public int sum() { return numbers.stream().mapToInt(Integer::intValue).sum(); } } ``` 在上述代码中,我们通过 `@Setup` 注解来初始化包含 1 百万个整数的列表。`@Benchmark` 注解标记了 `sum()` 方法,该方法将列表转换为流,然后通过 `mapToInt()` 方法将流中的元素映射为整数,并最终调用 `sum()` 方法计算它们的总和。 接下来,我们来看一下并行流的实现代码: ```java public class ParallelStreamBenchmark { private List<Integer> numbers; @Setup public void setup() { numbers = new ArrayList<>(); for (int i = 0; i < 1_000_000; i++) { numbers.add(i); } } @Benchmark public int sum() { return numbers.parallelStream().mapToInt(Integer::intValue).sum(); } } ``` 与串行流的实现代码类似,我们只需要将 `stream()` 方法替换为 `parallelStream()` 方法即可将流转换为并行流。 接下来,我们可以使用 jmh 测试这两个实现的性能差异。以下是测试类的代码: ```java public class StreamBenchmarkTest { @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) @Fork(1) @State(Scope.Thread) public static class SerialStreamBenchmarkTest { SerialStreamBenchmark serialStreamBenchmark = new SerialStreamBenchmark(); @Setup public void setup() { serialStreamBenchmark.setup(); } @Benchmark public int sum() { return serialStreamBenchmark.sum(); } } @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) @Fork(1) @State(Scope.Thread) public static class ParallelStreamBenchmarkTest { ParallelStreamBenchmark parallelStreamBenchmark = new ParallelStreamBenchmark(); @Setup public void setup() { parallelStreamBenchmark.setup(); } @Benchmark public int sum() { return parallelStreamBenchmark.sum(); } } public static void main(String[] args) throws RunnerException { Options options = new OptionsBuilder() .include(StreamBenchmarkTest.class.getSimpleName()) .build(); new Runner(options).run(); } } ``` 在上述代码中,我们使用了 `@BenchmarkMode`、`@OutputTimeUnit`、`@Warmup`、`@Measurement`、`@Fork` 和 `@State` 注解来配置 jmh 测试。其中,`@State` 注解指定了测试类的状态,`@Setup` 注解用于初始化测试数据。 最后,我们运行测试类,就可以得到并行流和串行流的性能测试结果了。 需要注意的是,实际的测试结果可能因为硬件环境和测试数据的不同而有所不同,这里只是提供了一个简单的示例来比较并行流和串行流在 jmh 测试中的性能差异。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值