多線程實現報表的高效導出
多線程、線程池、並發包每當談起這些詞匯,可能不是在面試就是在準備面試的路上了。
有句話叫“面試造航母,工作擰螺絲“,確實很多情況下我們是用不到這些東西的,但是學好這些東西對我們的日常工作也可能會產生意想不到的好處的。
臨近年末,收拾了下手頭工作,趁著最後兩天有些閑暇,準備著手優化下前段時間業務人員反饋的部分報表導出速度過慢的問題。
報表的優化主要是涉及兩個方面,一個是SQL和數據庫層面的優化,另一個就是代碼層面的優化了,本文主要講述代碼層面利用多線程處理的一點小總結。
多線程實現的基礎知識
實現多線程的方式
- 繼承Thread類創建線程
- 實現Runnable接口創建線程
- 實現Callable接口創建線程
- 線程池的實現
JDK自帶的五種線程池的使用場景
-
newSingleThreadExecutor:一個單線程的線程池,可以用於需要保證順序執行的場景,並且只有一個線程在執行。
-
newFixedThreadPool:一個固定大小的線程池,可以用於已知並發壓力的情況下,對線程數做限制。
-
newCachedThreadPool:一個可以無限擴大的線程池,比較適合處理執行時間比較小的任務。
-
newScheduledThreadPool:可以延時啟動,定時啟動的線程池,適用於需要多個後臺線程執行周期任務的場景。
-
newWorkStealingPool:一個擁有多個任務隊列的線程池,可以減少連接數,創建當前可用cpu數量的線程來並行執行。
如何自定義線程池
在實際的使用過程中,一般我們都是用Executors去創建線程池,如果有一些其他的需求,比如指定線程池的拒絕策略,阻塞隊列的類型,線程名稱的前綴等等,我們可以采用自定義線程池的方式來解決。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) ;
-
corePoolSize:線程池大小,決定著新提交的任務是新開線程去執行還是放到任務隊列中,也是線程池的最最核心的參數。一般線程池開始時是沒有線程的,只有當任務來了並且線程數量小於corePoolSize才會創建線程。
-
maximumPoolSize:最大線程數,線程池能創建的最大線程數量。
-
keepAliveTime:在線程數量超過corePoolSize後,多余空閑線程的最大存活時間。
-
unit:時間單位
-
workQueue:存放來不及處理的任務的隊列,是一個BlockingQueue。
-
threadFactory:生產線程的工廠類,可以定義線程名,優先級等。
-
handler:拒絕策略,當任務來不及處理的時候,如何處理, 前面有講解。
execute和submit的區別
- execute適用於不需要關註返回值的場景,只需要將線程丟到線程池中去執行就可以了
- submit方法適用於需要關註返回值的場景,在線程執行結束會返回響應的結果值
其實這兩種方法的底層就是Runnable,Callable的實現。
多線程的一些基礎小知識,有興趣的同學可以園子裏翻翻其他同學的介紹,多線程、線程池、並發包這些東西無論是學習還是面試都是比較重要的。
報表優化案例
報表導出慢的原因探查
仔細檢查了需要優化的報表,發現因為這個報表的實時性要求比較高,同時涉及大量數據的計算操作,在優化了sql後效率還是無法達到滿意的程度,所以決定采用多線程的方式多個線程同時處理不同的業務邏輯,最後在合並數據返回,以達到提高效率的目的。
代碼解決方案
初步決定采用ExecutorService的submit方法,將一個復雜報表拆分為四個子線程執行並返回結果。同時采用並發包中的CountDownLatch做同步器,等待 四個子線程執行完畢後,再在主線程進行數據合並操作。假如每個子線程的執行時長在10分鐘左右,如果采用原先的串行方式的話,四個業務處理大概需要40分鐘左右,現在這種並行的方式執行只需要十分鐘的處理時間。
偽代碼實現
long startTime = DateUtils.getCurrentDateTime().getTime(); ExecutorService service = Executors.newFixedThreadPool(4); CountDownLatch latch = new CountDownLatch(4); Future<List<CapitalVO>> borrowIncrement = service.submit(new Callable<List<CapitalVO>>() { @Override public List<CapitalVO> call() throws Exception { List<CapitalVO> list = listBorrowIncrement(startDate, endDate); latch.countDown(); return list; } }); Future<List<OwnVO>> beceiveAccount = service.submit(new Callable<List<OwnVO>>() { @Override public List<OwnVO> call() throws Exception { List<OwnVO> list = listReceiveAccount(startDate, endDate); latch.countDown(); return list; } }); Future<List<OwnVO>> buaranteeAccount = service.submit(new Callable<List<OwnVO>>() { @Override public List<OwnVO> call() throws Exception { List<OwnVO> list = listGuaranteeAccount(startDate, endDate); latch.countDown(); return list; } }); Future<List<BorrowerVO>> borrowerRepayment = service.submit(new Callable<List<BorrowerVO>>() { @Override public List<BorrowerVO> call() throws Exception { List<BorrowerVO> list = listBorrowerRepayment(startDate, endDate); latch.countDown(); return list; } }); latch.await(); List<CapitalVO> borrowCapitalIncrement = borrowIncrement.get(); List<OwnVO> ownReceive = beceiveAccount.get(); List<OwnVO> ownAccountGuan = buaranteeAccount.get(); List<BorrowerVO> borrower = borrowerRepayment.get();
上述代碼利用CountDownLatch實現了線程同步,同時解決了原本串行執行時間較長的問題,在最終的效果上也是達到了預期的優化目標,比原報表的處理時長減少了四分之三的時間。
另外,有同學提出現在是實現了四個線程並行處理,處理時長大概在十分鐘左右。但是假如其中一個線程出現了報錯,不在需要其他線程繼續執行,這個時候該怎麽處理呢?
確實是存在這個情況的,其實我們可以利用Future對象的 cancel(boolean mayInterruptIfRunning)來中斷其他線程,底層其實還是thread.interrupt()的方法實現。
總結
總的來說技術方案上並沒有什麽特別的東西,但是有時候有沒有往這方面做就是一個思考的問題了。其實在工作中九成以上的人每天都是在做CRUD的業務,但是即便是CRUD每個人做出來的東西還是有所不同的。多思考多實踐,其實多線程並沒有那麽遙不可及,即便是簡單的報表,也是可以做出不一樣的東西的。
最後,新年臨近,祝福大家新年快樂,也希望自己能夠在新的一年做一個合格的creative worker。
多線程實現報表的高效導出