Java高并发编程详解:深入理解并发核心库
上QQ阅读APP看书,第一时间看更新

1.2 JMH快速入门

在开始学习JMH之前,我们先来看一个不太严谨的性能测试方案(也许你平时就是这么做的),比较常见的List有ArrayList和LinkedList,在选取这两个API作为你程序中的数据容器时,我们先对比一下这两者的add方法在性能上的差异,从而挑选出一个对性能较好的容器应用在程序开发之中。

1.2.1 用main方法进行测试

程序员通常会将一些简单的测试交给一个main函数来完成,比如对比两种List容器的add方法的性能时,我们将分别对其进行十组测试,每组测试都将会对List执行1 000 000次的add调用,然后通过最大值、最小值、平均值的方式对其进行对比(当然,这里并不考虑不同容器的内存开销,事实上,ArrayList和LinkedList在使用的过程中,它们的内存开销肯定是不一样的)。

程序代码:ArrayListVSLinkedList.java


package com.wangwenjun.concurrent.jmh;

import com.google.common.base.Stopwatch;
import com.google.common.base.Strings;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class ArrayListVSLinkedList
{
    private final static String DATA = "DUMMY DATA";
    private final static int MAX_CAPACITY = 1_000_000;
    private final static int MAX_ITERATIONS = 10;
    private static void test(List<String> list)
    {
        for (int i = 0; i < MAX_CAPACITY; i++)
        {
            list.add(DATA);
        }
    }

    private static void arrayListPerfTest(int iterations)
    {
        for (int i = 0; i < iterations; i++)
        {
            final List<String> list = new ArrayList<>();
            final Stopwatch stopwatch = Stopwatch.createStarted();
            test(list);
            System.out.println(stopwatch.stop()
                  .elapsed(TimeUnit.MILLISECONDS));
        }
    }

    private static void linkedListPerfTest(int iterations)
    {
        for (int i = 0; i < iterations; i++)
        {
            final List<String> list = new LinkedList<>();
            final Stopwatch stopwatch = Stopwatch.createStarted();
            test(list);
            System.out.println(stopwatch.stop()
                      .elapsed(TimeUnit.MILLISECONDS));
        }
    }

    public static void main(String[] args)
    {
        arrayListPerfTest(MAX_ITERATIONS);
        System.out.println(Strings.repeat("#", 100));
        linkedListPerfTest(MAX_ITERATIONS);
    }
}

运行上面的程序,我们会得出如下的数据(如图1-1所示)。

图1-1 ArrayList vs LinkedList add方法性能

▪ ArrayList:最大耗时304毫秒,最小耗时18毫秒,平均耗时83.2毫秒。

▪ LinkedList:最大耗时455毫秒,最小耗时21毫秒,平均耗时104毫秒。

乍一看ArrayList的add方法性能要好于LinkedList的add方法,事实上,ArrayList的随机读写性能确实要好于LinkedList(尤其是在ArrayList不进行内部扩容数组复制的情况下),LinkedList由于链表的设计,其delete操作的性能会好于ArrayList,无论怎样,我们的这种测试仍旧存在诸多问题,具体列举如下。

▪ 使用Stopwatch进行时间计算,其实是在Stopwatch内部记录了方法执行的开始纳秒数,这种操作本身会导致一些CPU时间的浪费。

▪ 在代码的运行过程中,JVM可能会对其进行运行时的优化,比如循环展开、运行时编译等,这样会导致某组未经优化的性能数据参与统计计算。

▪ arrayListPerfTest方法和linkedListPerfTest的运行环境并不公平,比如,在第一个测试方法执行的过程中或者执行结束后,其所在的JVM进程或许已经进行了profiler的优化,还有第一个测试方法所开辟的内存有可能也未被释放。

1.2.2 用JMH进行微基准测试

经过1.2.1节中的讨论,我们大体上知道要做一个严谨的基准测试并不是一件容易的事情,幸运的是,JMH的开发者已经为我们提供了对应的解决方案,本节就来看一下如何使用JMH对LinkedList和ArrayList的add方法进行性能测试。

首先,要将JMH的依赖加入我们的工程之中,笔者使用的是JMH的1.19版本,截至本书写作的时候,JMH最新最稳定的版本为1.19版。


<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.19</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.19</version>
    <scope>provided</scope>
</dependency>

程序代码:JMHExample01.java


package com.wangwenjun.concurrent.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.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class JMHExample01
{
    private final static String DATA = "DUMMY DATA";

    private List<String> arrayList;
    private List<String> linkedList;

    @Setup(Level.Iteration)
    public void setUp()
    {
        this.arrayList = new ArrayList<>();
        this.linkedList = new LinkedList<>();
    }

    @Benchmark
    public List<String> arrayListAdd()
    {
        this.arrayList.add(DATA);
        return arrayList;
    }

    @Benchmark
    public List<String> linkedListAdd()
    {
        this.linkedList.add(DATA);
        return this.linkedList;
    }

    public static void main(String[] args) throws RunnerException
    {
        final Options opts = new
     OptionsBuilder().include(JMHExample01.class.getSimpleName())
                    .forks(1)
                .measurementIterations(10)
                .warmupIterations(10)
                .build();
        new Runner(opts).run();
    }
}

上面的程序中,我们使用了大量的JMH API,也许暂时你还不太熟悉甚至还不知道它们的用法,不过不必担心,后文中会逐一进行讲解。首先运行上面的程序,会得到非常多的信息输出,程序输出如下所示。

程序输出:JMHExample01.java


# JMH version: 1.19
# VM version: JDK 1.8.0_40-ea, VM 25.40-b11
# VM invoker: D:\Program Files\Java\jdk1.8.0_40\jre\bin\java.exe
# VM options: -javaagent:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition
2017.2\lib\idea_rt.jar=52719:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition
2017.2\bin -Dfile.encoding=UTF-8
# Warmup: 10 iterations, 1 s each
# Measurement: 10 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.wangwenjun.concurrent.jmh.JMHExample01.arrayListAdd

# Run progress: 0.00% complete, ETA 00:00:40
# Fork: 1 of 1
# Warmup Iteration   1: 0.074 us/op
# Warmup Iteration   2: 0.412 us/op
# Warmup Iteration   3: 0.163 us/op
# Warmup Iteration   4: 0.492 us/op
# Warmup Iteration   5: 0.058 us/op
# Warmup Iteration   6: 0.057 us/op
# Warmup Iteration   7: 0.042 us/op
# Warmup Iteration   8: 0.092 us/op
# Warmup Iteration   9: 0.042 us/op
# Warmup Iteration  10: 0.056 us/op
Iteration   1: 0.043 us/op
Iteration   2: 0.083 us/op
Iteration   3: 0.046 us/op
Iteration   4: 0.038 us/op
Iteration   5: 0.034 us/op
Iteration   6: 0.085 us/op
Iteration   7: 0.037 us/op
Iteration   8: 0.042 us/op
Iteration   9: 0.042 us/op
Iteration  10: 0.035 us/op

Result "com.wangwenjun.concurrent.jmh.JMHExample01.arrayListAdd":
  0.048  ±(99.9%) 0.029 us/op [Average]
  (min, avg, max) = (0.034, 0.048, 0.085), stdev = 0.019
  CI (99.9%): [0.020, 0.077] (assumes normal distribution)

# JMH version: 1.19
# VM version: JDK 1.8.0_40-ea, VM 25.40-b11
# VM invoker: D:\Program Files\Java\jdk1.8.0_40\jre\bin\java.exe
# VM options: -javaagent:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition
2017.2\lib\idea_rt.jar=52719:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition
2017.2\bin -Dfile.encoding=UTF-8
# Warmup: 10 iterations, 1 s each
# Measurement: 10 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.wangwenjun.concurrent.jmh.JMHExample01.linkedListAdd

# Run progress: 50.00% complete, ETA 00:00:27
# Fork: 1 of 1
# Warmup Iteration   1: 0.174 us/op
# Warmup Iteration   2: 2.838 us/op
# Warmup Iteration   3: 0.995 us/op
# Warmup Iteration   4: 0.953 us/op
# Warmup Iteration   5: 0.202 us/op
# Warmup Iteration   6: 1.029 us/op
# Warmup Iteration   7: 0.238 us/op
# Warmup Iteration   8: 0.191 us/op
# Warmup Iteration   9: 0.192 us/op
# Warmup Iteration  10: 0.177 us/op
Iteration   1: 1.040 us/op
Iteration   2: 0.192 us/op
Iteration   3: 1.067 us/op
Iteration   4: 0.186 us/op
Iteration   5: 1.107 us/op
Iteration   6: 0.188 us/op
Iteration   7: 1.023 us/op
Iteration   8: 0.179 us/op
Iteration   9: 1.041 us/op
Iteration  10: 0.220 us/op

Result "com.wangwenjun.concurrent.jmh.JMHExample01.linkedListAdd":
  0.624  ±(99.9%) 0.688 us/op [Average]
  (min, avg, max) = (0.179, 0.624, 1.107), stdev = 0.455
  CI (99.9%): [ ≈ 0, 1.313] (assumes normal distribution)

# Run complete. Total time: 00:02:19

Benchmark                   Mode  Cnt  Score   Error  Units
JMHExample01.arrayListAdd   avgt   10  0.048  ± 0.029  us/op
JMHExample01.linkedListAdd  avgt   10  0.624  ± 0.688  us/op

虽然输出的信息很多,但是目前我们只需要查看输出的最后两行,大体上,我们从这两行信息可以发现arrayListAdd方法的调用平均响应时间为0.048微秒,误差在0.029微秒,而linkedListAdd方法的调用平均响应时间为0.624微秒,误差在0.688微秒,很明显,前者的性能是要高于后者的,虽然从结果上来看,这与我们之前利用main函数的测试方法得出的结论是一致的,但是显然JMH要严谨科学很多。