性能调优之JMH必知必会4:JMH的高级用法
- JMH必知必会系列文章(持续更新)
- 一、前言
- 二、JMH的高级用法
- 1、添加JMH依赖包
- 2、Asymmetric Benchmark
- 3、Interrupts Benchmark
- 4、几大线程安全Map的性能对比
JMH必知必会系列文章(持续更新)
- 性能调优之JMH必知必会1:什么是JMH
- 性能调优之JMH必知必会2:JMH的基本用法
- 性能调优之JMH必知必会3:编写正确的微基准测试用例
- 性能调优之JMH必知必会5:JMH的Profiler
一、前言
在前面三篇文章中分别介绍了什么是JMH、JMH的基本法和编写正确的微基准测试用例。现在来介绍JMH的一些高级用法。【单位换算:1秒(s)=1000000微秒(us)=1000000000纳秒(ns)
】
官方JMH源码(包含样例,在jmh-samples包里)下载地址:https://github.com/openjdk/jmh/tags。
官方JMH样例在线浏览地址:http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/。
本文内容参考书籍《Java高并发编程详解:深入理解并发核心库》,作者为 汪文君 ,读者有需要可以去购买正版书籍。
本文由 @大白有点菜 原创,请勿盗用,转载请说明出处!如果觉得文章还不错,请点点赞,加关注,谢谢!
二、JMH的高级用法
1、添加JMH依赖包
在Maven仓库中搜索依赖包jmh-core
和 jmh-generator-annprocess
,版本为 1.36
。需要注释 jmh-generator-annprocess 包中的“<scope>test</scope>”,不然项目运行会报错。
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.36</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.36</version>
</dependency>
2、Asymmetric Benchmark
我们编写的所有基准测试都会被JMH框架根据方法名的字典顺序排序之后串行执行,然而有些时候我们会想要对某个类的读写方法并行执行,比如,我们想要在修改某个原子变量的时候又有其他线程对其进行读取操作。
【Asymmetric Benchmark样例代码 - 代码】
package cn.zhuangyt.javabase.jmh;
import org.openjdk.jmh.annotations.*;
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;
import java.util.concurrent.atomic.AtomicInteger;
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Group)
public class JmhTestApp15_AsymmetricBenchmark {
private AtomicInteger counter;
@Setup
public void init()
{
this.counter = new AtomicInteger();
}
@GroupThreads(5)
@Group("q")
@Benchmark
public void inc()
{
this.counter.incrementAndGet();
}
@GroupThreads(5)
@Group("q")
@Benchmark
public int get()
{
return this.counter.get();
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JmhTestApp15_AsymmetricBenchmark.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
【Asymmetric Benchmark样例代码 - 代码运行结果】
Benchmark Mode Cnt Score Error Units
JmhTestApp15_AsymmetricBenchmark.q avgt 5 0.083 ± 0.002 us/op
JmhTestApp15_AsymmetricBenchmark.q:get avgt 5 0.034 ± 0.002 us/op
JmhTestApp15_AsymmetricBenchmark.q:inc avgt 5 0.132 ± 0.006 us/op
我们在对AtomicInteger进行自增操作的同时又会对其进行读取操作,这就是我们经常见到的高并发环境中某些API的操作方式,同样也是线程安全存在隐患的地方。5个线程对AtomicInteger执行自增操作,5个线程对AtomicInteger执行读取时的性能输出说明如下:
- group q(5个读线程,5个写线程)的平均响应时间为0.083us,误差为0.002。
- group q(5个读线程)同时读取AtomicInteger变量的速度为0.034us,误差为0.002。
- group q(5个写线程)同时修改AtomicInteger变量的速度为0.132us,误差为0.006。
【附上官方Asymmetric样例(JMHSample_15_Asymmetric) - 代码】
package cn.zhuangyt.javabase.jmh;
import org.openjdk.jmh.annotations.*;
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;
import java.util.concurrent.atomic.AtomicInteger;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Group)
public class JmhTestApp15_Asymmetric {
private AtomicInteger counter;
@Setup
public void up() {
counter = new AtomicInteger();
}
@Benchmark
@Group("g")
@GroupThreads(3)
public int inc() {
return counter.incrementAndGet();
}
@Benchmark
@Group("g")
@GroupThreads(1)
public int get() {
return counter.get();
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JmhTestApp15_Asymmetric.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
【附上官方Asymmetric样例(JMHSample_15_Asymmetric) - 代码运行结果】
Benchmark Mode Cnt Score Error Units
JmhTestApp15_Asymmetric.g avgt 5 49.144 ± 0.270 ns/op
JmhTestApp15_Asymmetric.g:get avgt 5 27.448 ± 2.089 ns/op
JmhTestApp15_Asymmetric.g:inc avgt 5 56.375 ± 0.678 ns/op
JmhTestApp15_AsymmetricBenchmark.q avgt 5 0.082 ± 0.003 us/op
JmhTestApp15_AsymmetricBenchmark.q:get avgt 5 0.049 ± 0.002 us/op
JmhTestApp15_AsymmetricBenchmark.q:inc avgt 5 0.115 ± 0.007 us/op
【官方Asymmetric样例(JMHSample_15_Asymmetric)注解 - 谷歌和百度翻译互补】
到目前为止,所有测试都是对称的:在所有线程中执行相同的代码。有时,您需要非对称测试。JMH为此提供了@Group的概念,它可以将多个方法绑定在一起,所有线程都分布在测试方法中。
每个执行组包含一个或多个线程。 特定执行组中的每个线程执行@Group 注释的@Benchmark 方法之一。多个执行组可能参与运行。 运行中的总线程数四舍五入为执行组大小,这将只允许完整的执行组。
请注意,两个状态范围:Scope.Benchmark 和 Scope.Thread 并未涵盖此处的所有用例——您要么共享状态中的所有内容,要么不共享任何内容。为了打破这一点,我们有中间地带 Scope.Group,它标记要在执行组内共享的状态,但不在执行组之间共享。
将所有这些放在一起,下面的示例意味着:
a) 定义执行组"g",3个线程执行 inc(),1个线程执行 get(),每组4个线程;
b) 如果我们用 4 个线程运行这个测试用例,那么我们将有一个单独的执行组。 一般4*N个线程运行会创建N个执行组等;
c) 每个执行组共享一个 @State 实例:即执行组在组内共享计数器,但不跨组。
3、Interrupts Benchmark
前面的例子中为大家演示了多线程情况下同时对 AtomicInteger 执行读写操作的情况,虽然基准测试能够顺利地运行,但是有些时候我们想要执行某些容器的读写操作时可能会引起阻塞,这种阻塞并不是容器无法保证线程安全问题引起的,而是由JMH框架的机制引起的。
【Interrupts Benchmark样例 - 代码】
package cn.zhuangyt.javabase.jmh;
import org.openjdk.jmh.annotations.*;
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.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Group)
public class JmhTestApp16_InterruptsBenchmark {
private BlockingQueue<Integer> queue;
private final static int VALUE = Integer.MAX_VALUE;
@Setup
public void init()
{
this.queue = new ArrayBlockingQueue<>(10);
}
@GroupThreads(5)
@Group("blockingQueue")
@Benchmark
public void put()
throws InterruptedException
{
this.queue.put(VALUE);
}
@GroupThreads(5)
@Group("blockingQueue")
@Benchmark
public int take()
throws InterruptedException
{
return this.queue.take();
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JmhTestApp16_InterruptsBenchmark.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
在 JmhTestApp16_InterruptsBenchmark 中我们针对BlockingQueue同时进行读(take)和写(put)的操作,但是很遗憾,在某些情况下(或许是第一次运行时)程序会出现长时间的阻塞,对于每一批次的Measurement,当然也包括Warmup中,put和take方法都会同时被多线程执行。想象一下,假设put方法最先执行结束,take方法无法再次从blocking queue中获取元素的时候将会一直阻塞下去,同样,take方法最先执行结束后,put方法在放满10个元素后再也无法存入新的元素,进而进入了阻塞状态,这两种情况都会等到每一次iteration(批次)超时(默认是10分钟)后才能继续往下执行。
难道我们就没有办法测试高并发容器在线程挂起时的性能了吗?事实上,JMH的设计者们早就为我们想好了对应的解决方案,我们可以通过设置Options的timeout来强制让每一个批次的度量超时,超时的基准测试数据将不会被纳入统计之中,这也是JMH的另外一个严谨之处。
【Interrupts Benchmark增加超时参数样例 - 代码】
package cn.zhuangyt.javabase.jmh;
import org.openjdk.jmh.annotations.*;
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.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Group)
public class JmhTestApp16_InterruptsBenchmark {
private BlockingQueue<Integer> queue;
private final static int VALUE = Integer.MAX_VALUE;
@Setup
public void init()
{
this.queue = new ArrayBlockingQueue<>(10);
}
@GroupThreads(5)
@Group("blockingQueue")
@Benchmark
public void put()
throws InterruptedException
{
this.queue.put(VALUE);
}
@GroupThreads(5)
@Group("blockingQueue")
@Benchmark
public int take()
throws InterruptedException
{
return this.queue.take();
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JmhTestApp16_InterruptsBenchmark.class.getSimpleName())
.timeout(TimeValue.seconds(10))
.build();
new Runner(opt).run();
}
}
【Interrupts Benchmark增加超时参数样例 - 代码运行结果】
# JMH version: 1.36
# VM version: JDK 1.8.0_281, Java HotSpot(TM) 64-Bit Server VM, 25.281-b09
# VM invoker: D:\Develop\JDK\jdk1.8.0_281\jre\bin\java.exe
# VM options: -javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2021.1.3\lib\idea_rt.jar=13710:C:\Program Files\JetBrains\IntelliJ IDEA 2021.1.3\bin -Dfile.encoding=UTF-8
# Blackhole mode: full + dont-inline hint (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
##########设置超时时间为10秒
# Timeout: 10 s per iteration, ***WARNING: The timeout might be too low!***
# Threads: 10 threads (1 group; 5x "put", 5x "take" in each group), will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: cn.zhuangyt.javabase.jmh.JmhTestApp16_InterruptsBenchmark.blockingQueue
# Run progress: 0.00% complete, ETA 00:01:40
# Fork: 1 of 1
# Warmup Iteration 1: (benchmark timed out, interrupted 1 times) 16.056 ±(99.9%) 13.176 us/op
# Warmup Iteration 2: 22.701 ±(99.9%) 28.283 us/op
# Warmup Iteration 3: 13.663 ±(99.9%) 7.399 us/op
# Warmup Iteration 4: (benchmark timed out, interrupted 1 times) 13.507 ±(99.9%) 8.866 us/op
# Warmup Iteration 5: (benchmark timed out, interrupted 1 times) 15.758 ±(99.9%) 14.186 us/op
##########第一个批次的执行由于阻塞超时被中断,但是阻塞所耗费的CPU时间并未纳入统计
Iteration 1: 12.763 ±(99.9%) 5.298 us/op
put: 12.131 ±(99.9%) 9.317 us/op
take: 13.395 ±(99.9%) 17.552 us/op
Iteration 2: 17.265 ±(99.9%) 16.655 us/op
put: 17.173 ±(99.9%) 50.839 us/op
take: 17.357 ±(99.9%) 38.260 us/op
Iteration 3: 19.929 ±(99.9%) 21.201 us/op
put: 18.791 ±(99.9%) 55.449 us/op
take: 21.067 ±(99.9%) 58.632 us/op
Iteration 4: 19.870 ±(99.9%) 21.256 us/op
put: 21.154 ±(99.9%) 71.631 us/op
take: 18.586 ±(99.9%) 37.452 us/op
Iteration 5: 13.857 ±(99.9%) 8.801 us/op
put: 14.860 ±(99.9%) 29.325 us/op
take: 12.854 ±(99.9%) 15.272 us/op
Result "cn.zhuangyt.javabase.jmh.JmhTestApp16_InterruptsBenchmark.blockingQueue":
16.737 ±(99.9%) 12.825 us/op [Average]
(min, avg, max) = (12.763, 16.737, 19.929), stdev = 3.331
CI (99.9%): [3.912, 29.562] (assumes normal distribution)
Secondary result "cn.zhuangyt.javabase.jmh.JmhTestApp16_InterruptsBenchmark.blockingQueue:put":
16.822 ±(99.9%) 13.425 us/op [Average]
(min, avg, max) = (12.131, 16.822, 21.154), stdev = 3.487
CI (99.9%): [3.396, 30.247] (assumes normal distribution)
Secondary result "cn.zhuangyt.javabase.jmh.JmhTestApp16_InterruptsBenchmark.blockingQueue:take":
16.652 ±(99.9%) 13.445 us/op [Average]
(min, avg, max) = (12.854, 16.652, 21.067), stdev = 3.492
CI (99.9%): [3.207, 30.097] (assumes normal distribution)
# Run complete. Total time: 00:01:49
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
JmhTestApp16_InterruptsBenchmark.blockingQueue avgt 5 16.737 ± 12.825 us/op
JmhTestApp16_InterruptsBenchmark.blockingQueue:put avgt 5 16.822 ± 13.425 us/op
JmhTestApp16_InterruptsBenchmark.blockingQueue:take avgt 5 16.652 ± 13.445 us/op
观察输出结果会发现当出现阻塞时,JMH最多等待指定的超时时间会继续执行而不是像之前那样陷入长时间的阻塞。第一个批次(Iteration 1)的执行由于阻塞超时被中断,但是阻塞所耗费的CPU时间并未纳入统计。
【附上官方Interrupts样例(JMHSample_30_Interrupts) - 代码】
package cn.zhuangyt.javabase.jmh;
import org.openjdk.jmh.annotations.*;
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 org.openjdk.jmh.runner.options.TimeValue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Group)
public class JmhTestApp16_Interrupts {
private BlockingQueue<Integer> q;
@Setup
public void setup() {
q = new ArrayBlockingQueue<>(1);
}
@Group("Q")
@Benchmark
public Integer take() throws InterruptedException {
return q.take();
}
@Group("Q")
@Benchmark
public void put() throws InterruptedException {
q.put(42);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JmhTestApp16_Interrupts.class.getSimpleName())
.threads(2)
.forks(5)
.timeout(TimeValue.seconds(10))
.build();
new Runner(opt).run();
}
}
【附上官方Interrupts样例(JMHSample_30_Interrupts) - 代码运行结果】
Benchmark Mode Cnt Score Error Units
JmhTestApp16_Interrupts.Q avgt 25 6773.746 ± 753.955 ns/op
JmhTestApp16_Interrupts.Q:put avgt 25 6773.746 ± 753.955 ns/op
JmhTestApp16_Interrupts.Q:take avgt 25 6773.745 ± 753.954 ns/op
JmhTestApp16_InterruptsBenchmark.blockingQueue avgt 25 15.970 ± 3.405 us/op
JmhTestApp16_InterruptsBenchmark.blockingQueue:put avgt 25 16.128 ± 4.533 us/op
JmhTestApp16_InterruptsBenchmark.blockingQueue:take avgt 25 15.813 ± 2.921 us/op
【官方Interrupts样例(JMHSample_30_Interrupts)注解 - 谷歌和百度翻译互补】
JMH 还可以检测线程何时卡在基准测试中,并尝试强制中断基准线程。当 JMH 可以确定它不会影响测量时,它会尝试这样做。
在此示例中,我们要测量 ArrayBlockingQueue 的简单性能特征。不幸的是,在没有 harness 支持的情况下这样做会导致其中一个线程死锁,因为 take/put 的执行没有完美配对。对我们来说幸运的是,这两种方法都能很好地响应中断,因此我们可以依靠 JMH 为我们终止测量。尽管如此,JMH 仍会通知用户有关中断操作的信息,因此用户可以查看这些中断是否影响了测量。JMH 将在达到默认或用户指定的超时后开始发出中断。
这是 org.openjdk.jmh.samples.JMHSample_18_Control 的变体,但没有显式控制对象。 此示例适用于优雅地响应中断的方法。
4、几大线程安全Map的性能对比
对比几大线程安全Map的多线程下的读写性能。
【线程安全Map读写性能对比样例 - 代码】
package cn.zhuangyt.javabase.jmh;
import org.openjdk.jmh.annotations.*;
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.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.*;
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Group)
public class JmhTestApp16_Measure_Map_Performance {
@Param({"1", "2", "3", "4"})
private int type;
private Map<Integer, Integer> map;
@Setup
public void setUp()
{
switch (type)
{
case 1:
this.map = new ConcurrentHashMap<>();
break;
case 2:
this.map = new ConcurrentSkipListMap<>();
break;
case 3:
this.map = new Hashtable<>();
break;
case 4:
this.map = Collections.synchronizedMap(
new HashMap<>());
break;
default:
throw new IllegalArgumentException("Illegal map type.");
}
}
@Group("g")
@GroupThreads(5)
@Benchmark
public void putMap()
{
int random = randomIntValue();
this.map.put(random, random);
}
@Group("g")
@GroupThreads(5)
@Benchmark
public Integer getMap()
{
return this.map.get(randomIntValue());
}
private int randomIntValue()
{
return (int) Math.ceil(Math.random() * 600000);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JmhTestApp16_Measure_Map_Performance.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
【线程安全Map读写性能对比样例 - 代码运行结果】
Benchmark (type) Mode Cnt Score Error Units
JmhTestApp16_Measure_Map_Performance.g 1 avgt 5 2.402 ± 0.406 us/op
JmhTestApp16_Measure_Map_Performance.g:getMap 1 avgt 5 2.394 ± 0.484 us/op
JmhTestApp16_Measure_Map_Performance.g:putMap 1 avgt 5 2.409 ± 0.332 us/op
JmhTestApp16_Measure_Map_Performance.g 2 avgt 5 2.804 ± 0.571 us/op
JmhTestApp16_Measure_Map_Performance.g:getMap 2 avgt 5 2.951 ± 0.578 us/op
JmhTestApp16_Measure_Map_Performance.g:putMap 2 avgt 5 2.658 ± 0.565 us/op
JmhTestApp16_Measure_Map_Performance.g 3 avgt 5 3.745 ± 0.195 us/op
JmhTestApp16_Measure_Map_Performance.g:getMap 3 avgt 5 6.081 ± 0.436 us/op
JmhTestApp16_Measure_Map_Performance.g:putMap 3 avgt 5 1.408 ± 0.048 us/op
JmhTestApp16_Measure_Map_Performance.g 4 avgt 5 4.754 ± 2.204 us/op
JmhTestApp16_Measure_Map_Performance.g:getMap 4 avgt 5 8.171 ± 4.468 us/op
JmhTestApp16_Measure_Map_Performance.g:putMap 4 avgt 5 1.336 ± 0.148 us/op
我们可以看到,在 putMap 和 getMap 方法中,通过随机值的方式将取值作为 key 和 value 存入 map 中,同样也是通过随机值的方式将取值作为 key 从 map 中进行数据读取(当然读取的值可能并不存在)。还有我们在基准方法中进行了随机值的运算,虽然随机值计算所耗费的CPU时间也会被纳入基准结果的统计中,但是每一个 map 都进行了相关的计算,因此,我们可以认为大家还是站在了同样的起跑线上,故而可以对其忽略不计。
基准测试的数据可以表明,在5个线程同时进行 map 写操作,5个线程同时进行读操作时,参数 type=1 的性能是最佳的,也就是 ConcurrentHashMap 。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)