Java Concurrency #14: JUC — CountDownLatch & CyclicBarrier讓Thread們步調一致
對Thread Fork/Join再進一步的管理,CountDownLatch和CyclicBarrier,Multi Thread使用Thread Pool,分離任務與結合的利器
前言
上一篇文章介紹了,使用基本Thread做到將複雜的主任務切分不同的子任務,並且平行運行增加效率,如下兩圖
圖2
但是在Thread Pool篇我們有提到,在真實的運用場景不會單純的這樣呼叫Thread丟任務讓他執行,一定會搭配一個Thread Pool去做到執行緒管理,那如果增加了Thread Pool,開發者就無法簡單的呼叫Thread.join等待結果並且匯聚,這時候就需要JUC裡的小夥伴CountDownLatch幫忙了,所以本文的主要目標是介紹CountDownLatch如何將Thread Pool裡的Thread執行結果匯聚,並且在介紹CyclicBarrier,更進階的搭配Queue資料結構使用Fork and Join模式,讓效能在更上一層樓。
JUC — 讓Thread Pool裡的Thread步調一至 — CountDownLatch
CountDownLatch的基本理念就是設定一個數量上限為子任務數量的Counter,,每當Thread執行完後就會對此Counter — 1,如果Counter數值變成0之後就會往下運行,如果不為0的話則會block等待。
直接看範例程式碼
在上面的範例,特別為了任務創建了TaskSet物件,並且在main方法使用while(true)模擬真實不斷運轉的場景,在執行任務時每一次執行都給予新的CountDownLatch物件不會重複使用(重複使用會造成block失效,因為每次完成都會將counter變為0,主程序無法只透過一個counter判斷是否要往下執行,因為他不知道這是哪一次的子任務結果匯聚),而在操控Thread平行進行的程式碼都會在runnable中增加了countdownlatch的countDown,代表對counter — 1,而main thread則會呼叫await方法,代表block直到counter為0。
JUC — 將主要控制程序也切分為其中一個子任務 CyclicBarrier
上面介紹了使用Thread Pool將兩個需要耗費長時間的任務平行處理,接下來看下一張圖,
雖然將Task1和Task2分解並且平行化處理,但是如果在merge的Main Task也需要長時間處裡的話,那Task1和Task2平行處理的優點就會被縮小(需要等待Main Task處理完才能再次執行),這時候就可以考慮將Main Task也分配到一個Thread Pool去做平行處理,如下圖
這樣平行化處理的優勢就完全體現,當然如果系統對效能沒有如此要求的話,不一定要這樣設計,因為Concurrency(平行/多執行緒)的程式碼,並不好設計。
回歸正題,如果要達成上圖將Main Task也平行出來,則需要JUC中的CyclicBarrier輔助(當然要用純Thread設計也可以,只是可能會陷入平行精神崩潰),下面是範例程式碼
範例中,會替Main Task增加兩個Blocking Queue(多執行緒需要等待上一個Thread執行結束),而T1和T2在處理結束後會將結果放入Q中,Main Task到達Barrier的Count閥值,就會從Q中取資料進行merge。
CyclicBarrier的使用跟CountDownLatch差不多,主要差別在於當Count到閥值後,會將Main丟掉Thread Pool處理,而另一個則是在系統的Main Thread處理。
結論
- 可以將複雜的業務流程切分成不同的子任務,再由Main Task匯聚結果,如此可以提升效能,當然代價也是不好設計
- 如果Main Task也是長時間的話則可以考慮將Main Task也透過另一個Thread處理,JUC中的CyclicBarrier可以幫助開發者快速上手
- CyclicBarrier和CountDownLatch的差別在於,Barrier將任務放到Thread Pool平行處理,DownLatch是佔據系統的Main Thread處理