你覺得我的這段Java程式碼還有優化的空間嗎?
上週,因為要測試一個方法的在併發場景下的結果是不是符合預期,我寫了一段單元測試的程式碼。寫完之後截了個圖發了一個朋友圈,很多人表示短短的幾行程式碼,涉及到好幾個知識點。
還有人給出了一些優化的建議。那麼,這是怎樣的一段程式碼呢?涉及到哪些知識,又有哪些可以優化的點呢?
讓我們來看一下。
背景
先說一下背景,也就是要知道我們單元測試要測的這個方法具體是什麼樣的功能。我們要測試的服務是AssetService,被測試的方法是update方法。
update方法主要做兩件事,第一個是更新Asset、第二個是插入一條AssetStream。
更新Asset方法中,主要是更新資料庫中的Asset的資訊,這裡為了防止併發,使用了樂觀鎖。
插入AssetStream方法中,主要是插入一條AssetStream的流水資訊,為了防止併發,這裡在資料庫中增加了唯一性約束。
為了保證資料一致性,我們通過本地事務將這兩個操作包在同一個事務中。
以下是主要的程式碼,當然,這個方法中還會有一些前置的冪等性校驗、引數合法性校驗等,這裡就都省略了:
@Service
public
class
AssetServiceImpl
implements
AssetService
{
@Autowired
private TransactionTemplate transactionTemplate
;
@Override
public String
update
(
Asset asset
)
{
//引數檢查、冪等校驗、從資料庫取出最新asset等。
return transactionTemplate
.
execute
(status
-
>
{
updateAsset
(asset
)
;
return
insertAssetStream
(asset
)
;
}
)
;
}
}
因為這個方法可能會在併發場景中執行,所以該方法通過事務+樂觀鎖+唯一性約束做了併發控制。關於這部分的細節就不多講了,大家感興趣的話後面我再展開關於如何防併發的內容。
單測
因為上面這個方法是可能在併發場景中被呼叫的,所以需要在單測中模擬併發場景,於是,我就寫了以下的單元測試的程式碼:
public
class
AssetServiceImplTest
{
private
static ThreadFactory namedThreadFactory
=
new
ThreadFactoryBuilder
(
)
.
setNameFormat
(
"demo-pool-%d"
)
.
build
(
)
;
private
static ExecutorService pool
=
new
ThreadPoolExecutor
(
5
,
100
,
0L
, TimeUnit
.
MILLISECONDS
,
new
LinkedBlockingQueue
<Runnable
>
(
128
)
, namedThreadFactory
,
new
ThreadPoolExecutor
.AbortPolicy
(
)
)
;
@Autowired
AssetService assetService
;
@Test
public
void
test_updateConcurrent
(
)
{
Asset asset
=
getAsset
(
)
;
//引數的準備
//...
//併發場景模擬
CountDownLatch countDownLatch
=
new
CountDownLatch
(
10
)
;
AtomicInteger failedCount
=
new
AtomicInteger
(
)
;
//併發批量修改,只有一條可以修改成功
for
(int i
=
0
; i
<
10
; i
++
)
{
pool
.
execute
(
(
)
-
>
{
try
{
String streamNo
= assetService
.
update
(asset
)
;
}
catch
(Exception e
)
{
System
.out
.
println
(
"Error : "
+ e
)
;
failedCount
.
getAndIncrement
(
)
;
}
finally
{
countDownLatch
.
countDown
(
)
;
}
}
)
;
}
try
{
//主執行緒等子執行緒都執行完之後查詢最新的資產
countDownLatch
.
await
(
)
;
}
catch
(InterruptedException e
)
{
e
.
printStackTrace
(
)
;
}
Assert
.
assertEquals
(failedCount
.
intValue
(
)
,
9
)
;
// 從資料庫中反查出最新的Asset
// 再對關鍵欄位做注意校驗
}
}
以上,就是我做了簡化之後的單元測試的部分程式碼。因為要測併發場景,所以這裡面涉及到了很多併發相關的知識。
很多人之前和我說,併發相關的知識自己瞭解的很多,但是好像沒什麼機會寫併發的程式碼。其實,單元測試就是個很好的機會。
我們來看看上面的程式碼涉及到哪些知識點?
知識點
以上這段單元測試的程式碼中涉及到幾個知識點,我這裡簡單說一下。
執行緒池
這裡面因為要模擬併發的場景,所以需要用到多執行緒, 所以我這裡使用了執行緒池,而且我沒有直接用Java提供的Executors類建立執行緒池。
而是使用guava提供的ThreadFactoryBuilder來建立執行緒池,使用這種方式建立執行緒時,不僅可以避免OOM的問題,還可以自定義執行緒名稱,更加方便的出錯的時候溯源。(關於執行緒池建立的OOM問題)
CountDownLatch
因為我的單元測試程式碼中,希望在所有的子執行緒都執行之後,主執行緒再去檢查執行結果。
所以,如何使主執行緒阻塞,直到所有子執行緒執行完呢?這裡面用到了一個同步輔助類CountDownLatch。
用給定的計數初始化 CountDownLatch。由於呼叫了 countDown() 方法,所以在當前計數到達零之前,await 方法會一直受阻塞。(多執行緒中CountDownLatch的用法)
AtomicInteger
因為我在單測程式碼中,建立了10個執行緒,但是我需要保證只有一個執行緒可以執行成功。所以,我需要對失敗的次數做統計。
那麼,如何在併發場景中做計數統計呢,這裡用到了AtomicInteger,這是一個原子操作類,可以提供執行緒安全的操作方法。
異常處理
因為我們模擬了多個執行緒併發執行,那麼就一定會存在部分執行緒執行失敗的情況。
因為方法底層沒有對異常進行捕獲。所以需要在單測程式碼中進行異常的捕獲。
try
{
String streamNo
= assetService
.
update
(asset
)
;
}
catch
(Exception e
)
{
System
.out
.
println
(
"Error : "
+ e
)
;
failedCount
.
increment
(
)
;
}
finally
{
countDownLatch
.
countDown
(
)
;
}
這段程式碼中,try、catch、finall都用上了,而且位置是不能調換的。失敗次數的統計一定要放到catch中,countDownLatch的countDown也一定要放到finally中。
Assert
這個相信大家都比較熟悉,這就是JUnit中提供的斷言工具類,在單元測試時可以用做斷言。這就不詳細介紹了。
優化點
以上程式碼涉及到了很多知識點,但是,難道就沒有什麼優化點了嗎?
首先說一下,其實單元測試的程式碼對效能、穩定性之類的要求並不高,所謂的優化點,也並不是必要的。這裡只是說討論下,如果真的是要做到精益求精,還有什麼點可以優化呢?
使用LongAdder代替AtomicInteger
我的朋友圈的網友@zkx 提出,可以使用LongAdder代替AtomicInteger。
java.util.concurrency.atomic.LongAdder是Java8新增的一個類,提供了原子累計值的方法。而且在其Javadoc中也明確指出其效能要優於AtomicLong。
首先它有一個基礎的值base,在發生競爭的情況下,會有一個Cell陣列用於將不同執行緒的操作離散到不同的節點上去(會根據需要擴容,最大為CPU核數,即最大同時執行執行緒數),sum()會將所有Cell陣列中的value和base累加作為返回值。
核心的思想就是將AtomicLong一個value的更新壓力分散到多個value中去,從而降低更新熱點。所以在激烈的鎖競爭場景下,LongAdder效能更好。
增加併發競爭
朋友圈網友 Cafebabe 和 @普渡眾生的面癱青年 都提到同一個優化點,那就是如何增加併發競爭。
這個問題其實我在發朋友圈之前就有想到過,心中早已經有了答案,只不過有兩位朋友能夠幾乎同時提到這一點還是很不錯的。
我們來說說問題是什麼。
我們為了提升併發,使用執行緒池建立了多個執行緒,想讓多個執行緒併發執行被測試的方法。
但是,我們是在for迴圈中依次執行的,那麼理論上這10次update方法的呼叫是順序執行的。
當然,因為有CPU時間片的存在,這10個執行緒會爭搶CPU,真正執行的過程中還是會發生併發衝突的。
但是,為了穩妥起見,我們還是需要儘量模擬出多個執行緒同時發起方法呼叫的。
優化的方法也比較簡單,那就是在每一個update方法被呼叫之前都wait一下,直到所有的子執行緒都建立成功了,再開始一起執行。
這就還可以用都到我們前面講過的CountDownLatch。
所以,最終優化後的單測程式碼如下:
//主執行緒根據此CountDownLatch阻塞
CountDownLatch mainThreadHolder
=
new
CountDownLatch
(
10
)
;
//併發的多個子執行緒根據此CountDownLatch阻塞
CountDownLatch multiThreadHolder
=
new
CountDownLatch
(
1
)
;
//java學習交流:737251827 進入可領取學習資源及對十年開發經驗大佬提問,免費解答!
//失敗次數計數器
LongAdder failedCount
=
new
LongAdder
(
)
;
//併發批量修改,只有一條可以修改成功
for
(int i
=
0
; i
<
10
; i
++
)
{
pool
.
execute
(
(
)
-
>
{
try
{
//子執行緒等待,等待主執行緒通知後統一執行
multiThreadHolder
.
await
(
)
;
//呼叫被測試的方法
String streamNo
= assetService
.
update
(asset
)
;
}
catch
(Exception e
)
{
//異常發生時,對失敗計數器+1
System
.out
.
println
(
"Error : "
+ e
)
;
failedCount
.
increment
(
)
;
}
finally
{
//主執行緒的阻塞器奇數-1
mainThreadHolder
.
countDown
(
)
;
}
}
)
;
}
//通知所有子執行緒可以執行方法呼叫了
multiThreadHolder
.
countDown
(
)
;
try
{
//主執行緒等子執行緒都執行完之後查詢最新的資產池計劃
mainThreadHolder
.
await
(
)
;
}
catch
(InterruptedException e
)
{
e
.
printStackTrace
(
)
;
}
//斷言,保證失敗9次,則成功一次
Assert
.
assertEquals
(failedCount
.
intValue
(
)
,
9
)
;
// 從資料庫中反查出最新的Asset
// 再對關鍵欄位做注意校驗
以上,就是關於我的一次單元測試的程式碼所涉及到的知識點,以及目前所能想到的相關的優化點。
第一次被公眾號上近20萬讀者線上CodeReview,有點小小緊張。但是還是想問一下,對於這部分程式碼,你覺得還有什麼可以優化的地方嗎?
(全文完)
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70010294/viewspace-2847926/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 我覺得你可能真的還不會JavaJava
- python程式程式碼這樣加密保護,你覺得可以嗎?Python加密
- 你覺得虛擬DOM快嗎?還有沒有比它還快的方式?
- 想讓你的程式碼變得更加優雅嗎?
- 你還不懂 Tomcat 的優化嗎?Tomcat優化
- java工程師,你還記得我嗎?我是Servlet+jdbc+javaBJava工程師ServletJDBC
- 你覺得前端有必要寫文件嗎?前端
- 誕生兩年,這個市場還有想象空間嗎?
- Java9後String的空間優化Java優化
- Windows 的這款工具,有時讓我覺得 Mac 不是很香WindowsMac
- 還記得同桌的那個你嗎?
- 這是一段關乎你的程式碼:你的未來 我們正在參與
- 多次擠入暢銷榜TOP10的這個品類,還有想象空間嗎?
- 你覺得前端有必要懂後端語言嗎?前端後端
- 除了敲程式碼,你還有什麼副業嗎?
- 優化程式碼中大量的if/else,你有什麼方案?優化
- 覺得還是敲程式碼比較來勁
- 低程式碼/無程式碼的SaaS/CRM還有未來嗎?
- 【有趣】這段java程式碼太古怪Java
- 資料庫的這些效能優化,你做了嗎?資料庫優化
- #剛拿到阿里offer小夥的Java開發要求自述,你覺得你能去阿里嗎阿里Java
- 你見過哪些優雅的 Java 程式碼最佳化技巧?Java
- 優化你的程式碼結構 --- MVP優化MVP
- 做測試,除了點點滑鼠,你還有更大的發展空間!
- @程式設計師,你還記得當年高考時的樣子嗎?程式設計師
- CSS面試要點!看完你還覺得你已經學好CSS了嗎?CSS面試
- 你還記得資料庫三正規化嗎?資料庫
- 為什麼我覺得 Java 的 IO 很複雜?Java
- 2023年,學習網路安全還有發展空間嗎?
- 你們覺得雲棲社群好用嗎?
- 身為初學Java的你,這些IDE的優缺點你都知道嗎?JavaIDE
- 你覺得你每天最佳的工作時間是什麼時候?
- uni微信小程式優化,幾行程式碼就能省100kb的主包空間?微信小程式優化行程
- 初學Java,這三個階段你經歷過嗎?Java
- 這份優化清單,你都做了嗎?優化
- 如何優化我們的程式碼(vue專案)優化Vue
- 你還記得大明湖畔的oop原則嗎?OOP
- 這一篇 K8S(Kubernetes)叢集部署 我覺得還可以!!!K8S