去年,我们写过一篇关于JMH的入门使用的文章:Java基准测试工具JMH使用,今天我们再来聊一下关于JMH的高阶使用。主要我们会围绕着以下几点来讲:
- 对称并发测试
- 非对称并发测试
- 阻塞并发测试
- Map并发测试
关键词
@State 在很多时候我们需要维护一些状态内容,比如在多线程的时候我们会维护一个共享的状态,这个状态值可能会在每根线程中都一样,也有可能是每根线程都有自己的状态,JMH为我们提供了状态的支持。该注解只能用来标注在类上,因为类作为一个属性的载体。@State的状态值主要有以下几种:
Scope.Benchmark 该状态的意思是会在所有的Benchmark的工作线程中共享变量内容。
Scope.Group 同一个Group的线程可以享有同样的变量
Scope.Thread 每个线程都享有一份变量的副本,线程之间对于变量的修改不会相互影响
@Group 执行组的识别号
@GroupThreads 执行某个方法所需要的线程数量
对称并发测试
我们编写的所有基准测试都会被JMH框架根据方法名的字典顺序排序之后串行执行,然而有些时候我们会想要对某个类的读写方法并行执行,比如,我们想要在修改某个原子变量的时候又有其他线程对其进行读取操作。
@BenchmarkMode(Mode.AverageTime) @Fork(1) @Warmup(iterations = 5, time = 1) @Measurement(iterations = 5, time = 1) @OutputTimeUnit(TimeUnit.MICROSECONDS) @State(Scope.Group) public class SymmetricBenchmark { private AtomicLong counter; @Setup public void init() { this.counter = new AtomicLong(); } @GroupThreads(5) @Group("atomic") @Benchmark public void inc() { this.counter.incrementAndGet(); } @GroupThreads(5) @Group("atomic") @Benchmark public long get() { return this.counter.get(); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(SymmetricBenchmark.class.getSimpleName()) .build(); new Runner(opt).run(); } }
结果为:
Benchmark Mode Cnt Score Error Units SymmetricBenchmark.atomic avgt 5 0.126 ± 0.009 us/op SymmetricBenchmark.atomic:get avgt 5 0.062 ± 0.011 us/op SymmetricBenchmark.atomic:inc avgt 5 0.190 ± 0.011 us/op
我们在对AtomicLong进行自增操作的同时又会对其进行读取操作,这就是我们经常见到的高并发环境中某些API的操作方式,同样也是线程安全存在隐患的地方。5个线程对AtomicLong执行自增操作,5个线程对AtomicLong执行读取时的性能输出说明如下:
- group atomic(5个读线程,5个写线程)的平均响应时间为0.126 us,误差为0.009。
- group atomic(5个读线程)同时读取AtomicLong变量的速度为0.062 us,误差为0.011。
- group atomic(5个写线程)同时修改AtomicLong变量的速度为0.190 us,误差为0.011 。
非对称并发测试
有时,您需要达到非对称测试的目的。
@Fork(1) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) @Warmup(iterations = 5, time = 1) @Measurement(iterations = 5, time = 1) @State(Scope.Group) public class AsymmetricBenchMark { private AtomicLong counter; @Setup public void up() { counter = new AtomicLong(); } @Benchmark @Group("atomic") @GroupThreads(3) public long inc() { return counter.incrementAndGet(); } @Benchmark @Group("atomic") @GroupThreads(1) public long get() { return counter.get(); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(AsymmetricBenchMark.class.getSimpleName()) .build(); new Runner(opt).run(); } }
结果为:
Benchmark Mode Cnt Score Error Units AsymmetricBenchMark.atomic avgt 5 0.053 ± 0.003 us/op AsymmetricBenchMark.atomic:get avgt 5 0.025 ± 0.006 us/op AsymmetricBenchMark.atomic:inc avgt 5 0.062 ± 0.005 us/op
我们在对AtomicLong进行自增操作的同时又会对其进行读取操作,这就是我们经常见到的高并发环境中某些API的操作方式,同样也是线程安全存在隐患的地方。3个线程对AtomicLong执行自增操作,1个线程对AtomicLong执行读取时的性能输出说明如下:
- group atomic(1个读线程,3个写线程)的平均响应时间为0.053 us,误差为0.003 。
- group atomic(1个读线程)同时读取AtomicLong变量的速度为0.025 us,误差为0.006 。
- group atomic(3个写线程)同时修改AtomicLong变量的速度为0.062 us,误差为0.005 。
阻塞并发测试
有些时候我们想要执行某些容器的读写操作时可能会引起阻塞,比如blockqueue,在某些情况下程序会出现长时间的阻塞,这就严重影响了我们测试的结果,我们可以通过设置Options的timeout来强制让每一个批次的度量超时,超时的基准测试数据将不会被纳入统计之中。
以下测试,我们设置每批次如果超过10秒,就被认为超时不计入统计。
@Fork(1) @BenchmarkMode(Mode.AverageTime) @Warmup(iterations = 5, time = 1) @Measurement(iterations = 5, time = 1) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Group) public class InterruptBenchmark { private BlockingQueue<Integer> queue; private final static int VALUE = Integer.MAX_VALUE; @Setup public void init() { this.queue = new ArrayBlockingQueue<>(10); } @GroupThreads(5) @Group("queue") @Benchmark public void put() throws InterruptedException { this.queue.put(VALUE); } @GroupThreads(5) @Group("queue") @Benchmark public int take() throws InterruptedException { return this.queue.take(); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(InterruptBenchmark.class.getSimpleName()) // 将每个批次的超时时间设置为10秒 .timeout(TimeValue.milliseconds(10000)) .build(); new Runner(opt).run(); } }
结果为:
Benchmark Mode Cnt Score Error Units InterruptBenchmark.queue avgt 5 19204.384 ± 23024.739 ns/op InterruptBenchmark.queue:put avgt 5 14049.887 ± 49670.027 ns/op InterruptBenchmark.queue:take avgt 5 24358.880 ± 31679.280 ns/op
有些执行时被阻塞的结果就被忽略了,报告中会如下所示:
Iteration 5: (benchmark timed out, interrupted 1 times) 27130.727 ±(99.9%) 53300.757 ns/op
如果超时时间设置得过小,那么,会得到如下警告:
# Timeout: 1000 ms per iteration, ***WARNING: The timeout might be too low!***
Map并发测试
对比几大线程安全Map的多线程下的读写性能,以后类似的操作可以按照这个模板来。
@Fork(1) @BenchmarkMode(Mode.Throughput) @Warmup(iterations = 5, time = 1) @Measurement(iterations = 5, time = 1) @OutputTimeUnit(TimeUnit.SECONDS) @State(Scope.Group) public class MapBenchMark { @Param({"ConcurrentHashMap", "ConcurrentSkipListMap", "Hashtable", "Collections.synchronizedMap"}) private String type; private Map<Integer, Integer> map; @Setup public void setUp() { switch (type) { case "ConcurrentHashMap": this.map = new ConcurrentHashMap<>(); break; case "ConcurrentSkipListMap": this.map = new ConcurrentSkipListMap<>(); break; case "Hashtable": this.map = new Hashtable<>(); break; case "Collections.synchronizedMap": this.map = Collections.synchronizedMap( new HashMap<>()); break; default: throw new IllegalArgumentException("Illegal map type."); } } @Group("map") @GroupThreads(5) @Benchmark public void putMap() { int random = randomIntValue(); this.map.put(random, random); } @Group("map") @GroupThreads(5) @Benchmark public Integer getMap() { return this.map.get(randomIntValue()); } /** * 计算一个随机值用作Map中的Key和Value * * @return */ private int randomIntValue() { return (int) Math.ceil(Math.random() * 600000); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(MapBenchMark.class.getSimpleName()) .build(); new Runner(opt).run(); } }
结果如下:
Benchmark (type) Mode Cnt Score Error Units MapBenchMark.map ConcurrentHashMap thrpt 5 4903943.211 ± 208719.270 ops/s MapBenchMark.map:getMap ConcurrentHashMap thrpt 5 2442687.631 ± 251150.685 ops/s MapBenchMark.map:putMap ConcurrentHashMap thrpt 5 2461255.580 ± 260557.472 ops/s MapBenchMark.map ConcurrentSkipListMap thrpt 5 3471371.602 ± 334184.434 ops/s MapBenchMark.map:getMap ConcurrentSkipListMap thrpt 5 1710540.889 ± 196183.472 ops/s MapBenchMark.map:putMap ConcurrentSkipListMap thrpt 5 1760830.713 ± 263480.175 ops/s MapBenchMark.map Hashtable thrpt 5 1966883.854 ± 197740.289 ops/s MapBenchMark.map:getMap Hashtable thrpt 5 676801.687 ± 71672.436 ops/s MapBenchMark.map:putMap Hashtable thrpt 5 1290082.167 ± 174730.435 ops/s MapBenchMark.map Collections.synchronizedMap thrpt 5 1976316.282 ± 99878.457 ops/s MapBenchMark.map:getMap Collections.synchronizedMap thrpt 5 655744.125 ± 73634.788 ops/s MapBenchMark.map:putMap Collections.synchronizedMap thrpt 5 1320572.158 ± 75428.848 ops/s
我们可以看到,在 putMap 和 getMap 方法中,通过随机值的方式将取值作为 key 和 value 存入 map 中,同样也是通过随机值的方式将取值作为 key 从 map 中进行数据读取(当然读取的值可能并不存在)。还有我们在基准方法中进行了随机值的运算,虽然随机值计算所耗费的CPU时间也会被纳入基准结果的统计中,但是每一个 map 都进行了相关的计算,因此,我们可以认为大家还是站在了同样的起跑线上,故而可以对其忽略不计。
基准测试的数据可以表明,在5个线程同时进行 map 写操作,5个线程同时进行读操作时,参数 type=ConcurrentHashMap 的性能是最佳的 。
下一篇,将和大家介绍下JMH的profiler