JMH 微基准测试(性能测试)

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

准备工作
●JMH官方使用文档:OpenJDK: jmh
●【推荐】JMH GitHub地址(包含示例代码):https://github.com/openjdk/jmh
●IntelliJ(2020.2.3 社区版)
●Intellij 安装插件 JMH Java Microbenchmark Harness

关键参数介绍
测试程序注解介绍
●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实现的生产者消费者程序,将生产者消费者的队列区分开,减少不必要的争抢
结果理论值
程序二相比程序一来说,少了线程的争抢,吞吐量要高一些。
具体程序


    <properties>
      	<!-- 指定 jmh 版本号 -->
        <version.jmh-core>1.25.2</version.jmh-core>
		</properties>

    <dependencies>
        <!-- 引入 jmh -->
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>${version.jmh-core}</version>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>${version.jmh-core}</version>
        </dependency>
    </dependencies>
/*
 * 被测试程序 1
 */
package com.zhqy.juc.producerAndConsumer.jmh;

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

import java.util.LinkedList;

/**
 * <h3>通过 synchronized notify wait 关键字实现生产者、消费者工具</h3>
 *
 * @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<Object> linkedList = new LinkedList<>();

    public synchronized void push(Object x) {
        LOGGER.debug("生产者 - 进入对象锁 list数量:{}", linkedList.size());
        while (linkedList.size() >= MAX) {
            try {
                LOGGER.debug("生产者 - 开始休眠 list数量:{}", linkedList.size());
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 将数据放入
        linkedList.add(x);
        LOGGER.debug("生产者 - 放入数据 {} 后 list数量:{}", x, linkedList.size());
        notifyAll();
    }

    public synchronized Object pop() {
        LOGGER.debug("消费者 - 进入对象锁 list数量:{}", linkedList.size());
        while (linkedList.size() <= 0) {
            try {
                LOGGER.debug("消费者 - 开始休眠 list数量:{}", linkedList.size());
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 取出数据
        Object last = linkedList.removeLast();
        LOGGER.debug("消费者 - 消费 {},list数量:{}", last, linkedList.size());
        notifyAll();
        return last;
    }

}
/*
* 测试程序 1
*/

import org.openjdk.jmh.annotations.*;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 10, time = 5)
@Measurement(iterations = 100, time = 10)
@Threads(Threads.MAX)
@Fork(3)
@State(value = Scope.Thread)
@OutputTimeUnit(TimeUnit.SECONDS)
public class SynchronizedVersionTest {
    
    // 这一版已经解决问题
    private static final SynchronizedVersion TEST = new SynchronizedVersion();
    
    @Benchmark
    public void test() throws InterruptedException {
        // 记录总元素数量
        CountDownLatch countDownLatch = new CountDownLatch(100);
        
        // 用2个线程生产100个元素
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 50; j++) {
                    TEST.push(1);
                    try {
                        TimeUnit.MILLISECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        
        // 用100个线程消费所有元素
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                try {
                    TEST.pop();
                } finally {
                    // 每消费一次,不论成功失败,都进行计数
                    countDownLatch.countDown();
                }
            }).start();
        }
        
        // 阻断等待,等到所有元素消费完成后,自动放开
        countDownLatch.await();
    }
}
# 程序1 测试结果

Result "com.zhqy.juc.producerAndConsumer.jmh.SynchronizedVersionTest.test":
  36.339 ±(99.9%) 0.477 ops/s [Average]
  (min, avg, max) = (31.214, 36.339, 44.255), stdev = 2.486
  CI (99.9%): [35.862, 36.816] (assumes normal distribution)


# Run complete. Total time: 00:53:56

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                                              Mode  Cnt   Score   Error  Units
producerAndConsumer.jmh.SynchronizedVersionTest.test  thrpt  300  36.339 ± 0.477  ops/s
/*
 * 被测试程序 2
 */
package com.zhqy.juc.producerAndConsumer.jmh;

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

import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * <h3>通过 可重入锁 实现生产者、消费者,生产者、消费者独立使用通知队列</h3>
 *
 * @author wangshuaijing
 * @version 1.0.0
 * @date 2020/11/4 5:08 下午
 */
public class ReentrantLockVersion {

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

    /**
     * 容器中的最大数量
     */
    private static final int MAX = 20;

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

    /**
     * 定义一个 可重入锁
     */
    private final ReentrantLock reentrantLock = new ReentrantLock();
    /**
     * 为生产者定义一个独立的队列
     */
    private final Condition producerLock = reentrantLock.newCondition();
    /**
     * 为消费者定义一个独立的队列
     */
    private final Condition consumerLock = reentrantLock.newCondition();

    public void push(Object x) {
        try {
            reentrantLock.lock();
            LOGGER.debug("生产者 - 进入对象锁 list数量:{}", linkedList.size());

            while (linkedList.size() >= MAX) {
                LOGGER.debug("生产者 - 开始休眠 list数量:{}", linkedList.size());
                producerLock.await();
            }
            linkedList.add(x);
            LOGGER.debug("生产者 - 放入数据 {} 后 list数量:{}", x, linkedList.size());
            consumerLock.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }

    public Object pop() {
        try {
            reentrantLock.lock();
            LOGGER.debug("消费者 - 进入对象锁 list数量:{}", linkedList.size());

            while (linkedList.size() <= 0) {
                LOGGER.debug("消费者 - 开始休眠 list数量:{}", linkedList.size());
                consumerLock.await();
            }
            Object last = linkedList.removeLast();
            LOGGER.debug("消费者 - 消费 {},list数量:{}", last, linkedList.size());
            producerLock.signalAll();
            return last;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return null;
        } finally {
            reentrantLock.unlock();
        }
    }

}
/*
* 测试程序 2
*/
package com.zhqy.juc.producerAndConsumer.jmh;

import org.openjdk.jmh.annotations.*;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;


@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 10, time = 5)
@Measurement(iterations = 100, time = 10)
@Threads(Threads.MAX)
@Fork(3)
@State(value = Scope.Thread)
@OutputTimeUnit(TimeUnit.SECONDS)
public class ReentrantLockVersionTest {
    
    // 这一版已经解决问题
    private static final ReentrantLockVersion TEST = new ReentrantLockVersion();
    
    @Benchmark
    public void test() throws InterruptedException {
        // 记录总元素数量
        CountDownLatch countDownLatch = new CountDownLatch(100);
        
        // 用2个线程生产100个元素
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 50; j++) {
                    TEST.push(1);
                    try {
                        TimeUnit.MILLISECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        
        // 用100个线程消费所有元素
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                try {
                    TEST.pop();
                } finally {
                    // 每消费一次,不论成功失败,都进行计数
                    countDownLatch.countDown();
                }
            }).start();
        }
        
        // 阻断等待,等到所有元素消费完成后,自动放开
        countDownLatch.await();
    }
}
# 程序2测试结果

Result "com.zhqy.juc.producerAndConsumer.jmh.ReentrantLockVersionTest.test":
  39.203 ±(99.9%) 0.282 ops/s [Average]
  (min, avg, max) = (35.262, 39.203, 44.288), stdev = 1.472
  CI (99.9%): [38.921, 39.486] (assumes normal distribution)


# Run complete. Total time: 00:53:51

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                                               Mode  Cnt   Score   Error  Units
producerAndConsumer.jmh.ReentrantLockVersionTest.test  thrpt  300  39.203 ± 0.282  ops/s

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

特别说明
如果需要在springboot项目中运行,则需要通过程序启动springboot容器,然后从容器中获取自己需要的对象。具体程序如下:

/**
* setup初始化容器的时候只执行一次<br>
* Level.Trial 代表在 @Benchmark 注解的方法之前运行(具体运行的次数,由 @Threads 和 @State 共同决定。如果 @State 是 Scope.Thread,运行次数则为 @Threads 配置的线程数;如果 @State 是 Scope.Benchmark,运行次数则为1)<br>
* 运行次数值针对每一个 Fork 来说,新的Fork,会重新运行
*/
@Setup(Level.Trial)
public void init() {
    ConfigurableApplicationContext context = SpringApplication.run(BootApplication.class);
    
    xxxService = context.getBean(XxxService.class);
}

若有收获,就点个赞吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值