写本文主要是简单记录一下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);
}
若有收获,就点个赞吧