寫本文主要是簡單記錄一下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();@Benchmarkpublic 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.486CI (99.9%): [35.862, 36.816] (assumes normal distribution)# Run complete. Total time: 00:53:56REMEMBER: 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();@Benchmarkpublic 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.472CI (99.9%): [38.921, 39.486] (assumes normal distribution)# Run complete. Total time: 00:53:51REMEMBER: 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);
}
若有收獲,就點個贊吧