在线求CR,你觉得我这段Java代码还有优化的空间吗?
△Hollis, 一個對Coding有著獨特追求的人△
這是Hollis的第?362?篇原創分享
作者 l Hollis
來源 l Hollis(ID:hollischuang)
上周,因為要測試一個方法的在并發場景下的結果是不是符合預期,我寫了一段單元測試的代碼。寫完之后截了個圖發了一個朋友圈,很多人表示短短的幾行代碼,涉及到好幾個知識點。
還有人給出了一些優化的建議。那么,這是怎樣的一段代碼呢?涉及到哪些知識,又有哪些可以優化的點呢?
讓我們來看一下。
背景
先說一下背景,也就是要知道我們單元測試要測的這個方法具體是什么樣的功能。我們要測試的服務是AssetService,被測試的方法是update方法。
update方法主要做兩件事,第一個是更新Asset、第二個是插入一條AssetStream。
更新Asset方法中,主要是更新數據庫中的Asset的信息,這里為了防止并發,使用了樂觀鎖。
插入AssetStream方法中,主要是插入一條AssetStream的流水信息,為了防止并發,這里在數據庫中增加了唯一性約束。
為了保證數據一致性,我們通過本地事務將這兩個操作包在同一個事務中。
以下是主要的代碼,當然,這個方法中還會有一些前置的冪等性校驗、參數合法性校驗等,這里就都省略了:
@Servicepublic?class?AssetServiceImpl?implements?AssetService?{@Autowiredprivate?TransactionTemplate?transactionTemplate;@Overridepublic?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(20,?100,0L,?TimeUnit.MILLISECONDS,new?LinkedBlockingQueue<Runnable>(128),?namedThreadFactory,?new?ThreadPoolExecutor.AbortPolicy());@Autowiredprivate AssetService?assetService;@Testpublic?void?test_updateConcurrent()?{Asset?asset?=?getAsset();//參數的準備//...//并發場景模擬CountDownLatch?countDownLatch?=?new?CountDownLatch(10);AtomicInteger?atomicInteger?=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 方法會一直受阻塞。
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一下,直到所有的子線程都創建成功了,再開始一起執行。
這里就可以用到CyclicBarrier來實現,CyclicBarrier和CountDownLatch一樣,都是關于線程的計數器。
CountDownLatch: 一個線程(或者多個), 等待另外N個線程完成某個事情之后才能執行。?
CyclicBrrier: N個線程相互等待,任何一個線程完成之前,所有的線程都必須等待。
所以,最終優化后的單測代碼如下:
以上,就是關于我的一次單元測試的代碼所涉及到的知識點,以及目前所能想到的相關的優化點。
第一次被公眾號上近20萬讀者在線CodeReview,有點小小緊張。但是還是想問一下,對于這部分代碼,你覺得還有什么可以優化的地方嗎?
?
技術交流群
最近有很多人問,有沒有讀者交流群,想知道怎么加入。
最近我創建了一些群,大家可以加入。交流群都是免費的,只需要大家加入之后不要隨便發廣告,多多交流技術就好了。
目前創建了多個交流群,全國交流群、北上廣杭深等各地區交流群、面試交流群、資源共享群等。
有興趣入群的同學,可長按掃描下方二維碼,一定要備注:全國 Or 城市 Or 面試 Or 資源,根據格式備注,可更快被通過且邀請進群。
▲長按掃描
往期推薦別去外包
匯報下《Java工程師成神之路》的進展
學妹問我,并發問題的根源到底是什么?
如果你喜歡本文,
請長按二維碼,關注?Hollis.
轉發至朋友圈,是對我最大的支持。
點個?在看?
喜歡是一種感覺
在看是一種支持
↘↘↘
總結
以上是生活随笔為你收集整理的在线求CR,你觉得我这段Java代码还有优化的空间吗?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 学习进度二
- 下一篇: Java杂记之JVM内存模型