享學特邀作者:老顧
前言
我們小伙伴們是不是經常需要測試代碼的性能?小伙伴們是不是就會想到jmeter進行壓力測試一下,模擬N個用戶同時執行下,看看響應的時間多少。
今天老顧就用一個經典的比賽案例,來嘗試自行編寫個比賽業務,并隨便介紹一下CyclicBarrier和CountDownLatch區別。 可以根據比賽業務,可以抽象出性能測試工具類。
需求場景
有N個短跑選手進行比賽,希望記錄下來整個比賽的時間
1、第一名選手跑完比賽路程所花時間
2、最后一名選手跑完比賽路程所花時間
3、所有選手跑完比賽路程所花時間
4、每個選手跑完比賽路程所花時間之和
上面就是業務需求,那我們如何去實現?
分析需求
先上個圖,小伙伴們會更好理解
上圖中4個選手在進行賽跑,我們先看一下賽跑規則:
1、每個選手互不干擾,在自己的賽道上面進行跑步
2、選手在跑步前都需要活動一下,做好預備姿勢
3、選手是應該在裁判一聲槍響下,才能開始跑,不能提前跑
4、每個選手在跑到終點時,裁判會為每個選手記錄成績
5、比賽結束后,大會公布各個選手的成績,以及排名
設計思路
針對上面的規則,我們需要轉換成我們的程序設計:
1、選手間互不干擾,又同時進行賽跑。這個比較簡單,肯定用我們的Thread線程去解決。
2、記錄時間這個也比較簡單,利用System的時鐘
我們先到這邊,先上代碼,創建任務(賽跑)
RunTask代表選手的跑完比賽的耗時,為了真實模擬,加了隨機數,表示每個選手的耗時不一樣。繼續代碼,直接在main方法中,進行比賽
上面是一些基礎變量,記錄耗時。小伙伴要注意要用AtomicLong原子類,避免線程安全問題。下面的代碼就是比賽核心邏輯
1、創建線程(選手)
2、執行任務(賽跑)
3、記錄成績(耗時)
大會公布成績
執行比賽
小伙伴看看,是不是明顯不對啊,總耗時盡然為0,肯定有問題。
應該有人發現了,因為我們是在main方法中執行比賽的,其他線程單獨執行,主main線程執行完就終止了程序,而不會管其他線程有沒有結束。
這明顯和我們想要的不一樣,我們需要等所有的選手跑完,才能算比賽結束。那應該怎么優化呢?往下看
CyclicBarrier
我們這里引入一個知識點CyclicBarrier循環屏障,CyclicBarrier是一組線程互相等待,只有全部到達屏障點以后才能繼續執行。可以舉個生活場景
大巴車進入服務區進行休息,大巴車是要等到所有乘客上車后,才能發車。并不是一個人上車了就可以發車了。這個是所有乘客都知道的規則,互相等待所有人上車,才發車。循環的意思就是大巴車是一直這種規則,可重復利用
我們比賽的例子正好匹配,不是一個選手到達終點(屏障)就比賽結束,而是要等到所有選手到達終點才能結束比賽。
終點優化
根據上面的CyclicBarrier知識點,我們把代碼優化一下
一、增加CyclicBarrier變量
//定義屏障,為什么要加1?
final CyclicBarrier cb = new CyclicBarrier(nThreads + 1);復制代碼
為什么要加1?因為比賽裁判肯定先到終點(即主線程),那也需要等待,所以屏障點需要加1。
注意:這個是根據業務來的,如果設置屏障點,是根據業務邏輯設計的
二、選手跑完到屏障點
在選手跑完后,增加到達屏障點,等待
三、裁判到屏障點
這個代碼是在main主線程的,也就是裁判會先到,設置屏障點
終點優化結束,執行比賽吧
這個成績應該沒有問題,把大賽的成績都正確的顯示出來了。
系統耗時
我們小伙伴再仔細觀察下,上面的成績:
1、最后一名的耗時3397ms
2、比賽執行完耗時3398ms
相差1ms,當然我們這里設計是以毫秒為單位的,如果以納秒為單位他們的相差會比1ms少。這個不是關鍵,關鍵是其實最后一名跑完,其實就是比賽結束了。按照道理比賽執行耗時和最后一名的耗時是一樣的哦。
比賽執行多次,效果都一樣相差1ms。這個是為什么呢?就是因為系統耗時,我們看看比賽是在什么時候記時的,是全部選手開跑后才記時的。這邊就會存在誤差,因為系統執行也會耗時
//上面所有選手都已經開跑了
//整個比賽的開始時間
long startTime = System.currentTimeMillis();
//。。。
//整個比賽的結束時間
long endTime = System.currentTimeMillis();復制代碼
就是因為系統執行也是會消耗時間的。當然耗時不大就是幾納秒。小伙伴知道這個點后,會不會發現我們整個代碼還存在一個問題?
起點問題
我們一場比賽是要等所有選手準備好后,等待裁判發令后,才能開跑。我們來看一下我們的選手開跑代碼
選手是通過for循環創建出來的,而且創新好后,就執行start開跑了。這個是不對的。
小伙伴會說for循環很快的,沒關系吧。
這里是很有關系的,創建選手耗時是比較長的,而且循環體也有耗時。我們看一下之前的系統耗時,就是獲取結束時間也存在系統耗時,何況這里要分配內存、創建對象等。這樣對其他選手就不公平了,那怎么辦?老顧再分享一個并發控制類CountDownLatch
CountDownLatch
CountDownLatch是一個或一組線程等待其他線程完成各自的工作后再執行。舉個例子:
大家考場考試,有人提前交卷,但監考老師是不能走的,因為還有人沒有考完,只有等到所有人交卷了,老師才能走。是不是和CyclicBarrier類似,他們也有不同點,自行百度。
到我們這個案例中,應該要等待所有選手準備好后,才能開跑。
起點優化
增加變量,計數器為1,這個值是由我們的設計決定的
//增加CountDownLatch控制類
final CountDownLatch cdl = new CountDownLatch(1);復制代碼
選手預備等待
裁判發令
裁判發令后,所有的選手就會立即開跑,利用CountDownLatch達到了控制線程等待,一起執行。再執行比賽看看,也解決了系統耗時誤差的問題
-----------大會公布成績-------------
比賽選手數:4
------------------------
所有選手總耗時:6686ms
比賽執行完耗時:1920ms
第一名耗時:1281ms
最后一名耗時:1920ms復制代碼
總結
這篇文章只是個引子,把并發編程的兩個重要的類拋出來,主要介紹應用場景。具體類的用法,小伙伴們可以網上自行學習。還有CyclicBarrier和CountDownLatch兩者有相同點,有些場景可以替換使用。當然他們也有不同點,小伙伴們要注重關注。謝謝!!!
小伙伴是不是會說,那個性能測試工具類呢?其實上面已經把90%的核心代碼介紹了,把跑步抽象成外部傳入的任務,在加入循環執行次數就ok了,小伙伴可以自行完善。
END
歡迎長按下圖關注公眾號:享學課堂online!
公眾號后臺回復【java】,獲取精選準備的架構學習資料(視頻+文檔+架構筆記)