写本文主要是简单记录一下JMH的使用方式。JMH全名是Java Microbenchmark Harness,主要为在jvm上运行的程序进行基准测试的工具。作为一个开发人员,在重构代码,或者确认功能的性能时,可以选中这个工具。
本文场景:代码重构,测试新代码和旧代码的性能区别(QPS)

准备工作

关键参数介绍

测试程序注解介绍

  • BenchmarkMode:基准模式
    • 参数:value
      • Mode.Throughput:单位时间吞吐量(ops)
      • Mode.AverageTime:每次操作的平均时间
      • Mode.SampleTime:采样每个操作的时间
      • Mode.SingleShotTime:测量一次操作的时间
      • Mode.All:把上述的都列出来
  • Warmup:预热。在测试代码运行前运行,主要防止 程序初始化 或 jvm运行一段时间后自动优化代码 产生的影响。
    • 参数如下:
      • iterations:运行次数,默认:-1
      • time:每次运行的时间,默认:-1
      • timeUnit:运行时间的单位,默认:秒
      • batchSize:批处理大小,每次操作调用几次方法,默认:-1
  • Measurement:具体测试参数。同 Warmup
  • Threads:每个进程中的测试线程,可用于类或者方法上。一般选择为cpu乘以2。如果配置了 Threads.MAX ,代表使用 Runtime.getRuntime().availableProcessors() 个线程。
  • Fork:
    • 参数如下:
      • value参数:多少个进程来测试,如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试
  • State:状态共享范围。
    • 参数如下:
      • Scope.Thread:不和其他线程共享
      • Scope.Group:相同类型的所有实例将在同一组内的所有线程之间共享。每个线程组将提供自己的状态对象
      • Scope.Benchmark:相同类型的所有实例将在所有工作线程之间共享
  • OutputTimeUnit:默认时间单位

程序执行输出内容介绍

  • Result内容介绍(因为测试的是 ops,单位是 秒,下面的结果都是基于 ops/s 来说):
    • min:最小值
    • avg:平均值
    • max:最大值
    • stdev:标准差,对于平均值的分散程度(一般来讲越小越接近平均值)
  • 最终结果介绍:
    • Benchmark:jmh程序名
    • Mode:程序中设定的 BenchmarkMode
    • Cnt:总执行次数(不包含预热)
    • Score:格式是 结果是xxx ± xxx,单位时间内的结果,对本程序来说就是 ops/s
    • Error:
    • Units:单位

代码部分

程序介绍

  • 程序一:通过synchronized关键字实现的生产者消费者程序
  • 程序二:通过ReentrantLock实现的生产者消费者程序,将生产者消费者的队列区分开,减少不必要的争抢

    结果理论值

    程序二相比程序一来说,少了线程的争抢,吞吐量要高一些。

    具体程序

    1. <properties>
    2. <!-- 指定 jmh 版本号 -->
    3. <version.jmh-core>1.25.2</version.jmh-core>
    4. </properties>
    5. <dependencies>
    6. <!-- 引入 jmh -->
    7. <dependency>
    8. <groupId>org.openjdk.jmh</groupId>
    9. <artifactId>jmh-core</artifactId>
    10. <version>${version.jmh-core}</version>
    11. </dependency>
    12. <dependency>
    13. <groupId>org.openjdk.jmh</groupId>
    14. <artifactId>jmh-generator-annprocess</artifactId>
    15. <version>${version.jmh-core}</version>
    16. </dependency>
    17. </dependencies>

    ```java /*

    • 被测试程序 1 */ package com.zhqy.juc.producerAndConsumer.jmh;

import org.slf4j.Logger; import org.slf4j.LoggerFactory;

import java.util.LinkedList;

/**

  • 通过 synchronized notify wait 关键字实现生产者、消费者工具

    *
  • @author wangshuaijing
  • @version 1.0.0
  • @date 2020/11/4 5:08 下午 */ public class SynchronizedVersion {

    private static final Logger LOGGER = LoggerFactory.getLogger(SynchronizedVersion.class);

    private static final int MAX = 20;

    private final LinkedList linkedList = new LinkedList<>();

    public synchronized void push(Object x) {

    1. LOGGER.debug("生产者 - 进入对象锁 list数量:{}", linkedList.size());
    2. while (linkedList.size() >= MAX) {
    3. try {
    4. LOGGER.debug("生产者 - 开始休眠 list数量:{}", linkedList.size());
    5. wait();
    6. } catch (InterruptedException e) {
    7. e.printStackTrace();
    8. }
    9. }
    10. // 将数据放入
    11. linkedList.add(x);
    12. LOGGER.debug("生产者 - 放入数据 {} 后 list数量:{}", x, linkedList.size());
    13. notifyAll();

    }

    public synchronized Object pop() {

    1. LOGGER.debug("消费者 - 进入对象锁 list数量:{}", linkedList.size());
    2. while (linkedList.size() <= 0) {
    3. try {
    4. LOGGER.debug("消费者 - 开始休眠 list数量:{}", linkedList.size());
    5. wait();
    6. } catch (InterruptedException e) {
    7. e.printStackTrace();
    8. }
    9. }
    10. // 取出数据
    11. Object last = linkedList.removeLast();
    12. LOGGER.debug("消费者 - 消费 {},list数量:{}", last, linkedList.size());
    13. notifyAll();
    14. return last;

    }

    }

    1. ```
    2. # 程序1 测试结果
    3. Result "com.zhqy.juc.producerAndConsumer.jmh.SynchronizedVersionTest.test":
    4. 36.339 ±(99.9%) 0.477 ops/s [Average]
    5. (min, avg, max) = (31.214, 36.339, 44.255), stdev = 2.486
    6. CI (99.9%): [35.862, 36.816] (assumes normal distribution)
    7. # Run complete. Total time: 00:53:56
    8. REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
    9. why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
    10. experiments, perform baseline and negative tests that provide experimental control, make sure
    11. the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
    12. Do not assume the numbers tell you what you want them to tell.
    13. Benchmark Mode Cnt Score Error Units
    14. producerAndConsumer.jmh.SynchronizedVersionTest.test thrpt 300 36.339 ± 0.477 ops/s
    1. /*
    2. * 被测试程序 2
    3. */
    4. package com.zhqy.juc.producerAndConsumer.jmh;
    5. import org.slf4j.Logger;
    6. import org.slf4j.LoggerFactory;
    7. import java.util.LinkedList;
    8. import java.util.concurrent.locks.Condition;
    9. import java.util.concurrent.locks.ReentrantLock;
    10. /**
    11. * <h3>通过 可重入锁 实现生产者、消费者,生产者、消费者独立使用通知队列</h3>
    12. *
    13. * @author wangshuaijing
    14. * @version 1.0.0
    15. * @date 2020/11/4 5:08 下午
    16. */
    17. public class ReentrantLockVersion {
    18. private static final Logger LOGGER = LoggerFactory.getLogger(ReentrantLockVersion.class);
    19. /**
    20. * 容器中的最大数量
    21. */
    22. private static final int MAX = 20;
    23. private final LinkedList<Object> linkedList = new LinkedList<>();
    24. /**
    25. * 定义一个 可重入锁
    26. */
    27. private final ReentrantLock reentrantLock = new ReentrantLock();
    28. /**
    29. * 为生产者定义一个独立的队列
    30. */
    31. private final Condition producerLock = reentrantLock.newCondition();
    32. /**
    33. * 为消费者定义一个独立的队列
    34. */
    35. private final Condition consumerLock = reentrantLock.newCondition();
    36. public void push(Object x) {
    37. try {
    38. reentrantLock.lock();
    39. LOGGER.debug("生产者 - 进入对象锁 list数量:{}", linkedList.size());
    40. while (linkedList.size() >= MAX) {
    41. LOGGER.debug("生产者 - 开始休眠 list数量:{}", linkedList.size());
    42. producerLock.await();
    43. }
    44. linkedList.add(x);
    45. LOGGER.debug("生产者 - 放入数据 {} 后 list数量:{}", x, linkedList.size());
    46. consumerLock.signalAll();
    47. } catch (InterruptedException e) {
    48. e.printStackTrace();
    49. } finally {
    50. reentrantLock.unlock();
    51. }
    52. }
    53. public Object pop() {
    54. try {
    55. reentrantLock.lock();
    56. LOGGER.debug("消费者 - 进入对象锁 list数量:{}", linkedList.size());
    57. while (linkedList.size() <= 0) {
    58. LOGGER.debug("消费者 - 开始休眠 list数量:{}", linkedList.size());
    59. consumerLock.await();
    60. }
    61. Object last = linkedList.removeLast();
    62. LOGGER.debug("消费者 - 消费 {},list数量:{}", last, linkedList.size());
    63. producerLock.signalAll();
    64. return last;
    65. } catch (InterruptedException e) {
    66. e.printStackTrace();
    67. return null;
    68. } finally {
    69. reentrantLock.unlock();
    70. }
    71. }
    72. }
    1. # 程序2测试结果
    2. Result "com.zhqy.juc.producerAndConsumer.jmh.ReentrantLockVersionTest.test":
    3. 39.203 ±(99.9%) 0.282 ops/s [Average]
    4. (min, avg, max) = (35.262, 39.203, 44.288), stdev = 1.472
    5. CI (99.9%): [38.921, 39.486] (assumes normal distribution)
    6. # Run complete. Total time: 00:53:51
    7. REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
    8. why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
    9. experiments, perform baseline and negative tests that provide experimental control, make sure
    10. the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
    11. Do not assume the numbers tell you what you want them to tell.
    12. Benchmark Mode Cnt Score Error Units
    13. producerAndConsumer.jmh.ReentrantLockVersionTest.test thrpt 300 39.203 ± 0.282 ops/s

    最终结果

    • 与理论值相同,程序二(通过ReentrantLock,分开生产者、消费者队列)降低了不必要的线程的争抢,增加了最终的吞吐量。
    • jmh还可以用来排查并发问题 ^_^