Java 和其餘平臺相比最大的優點在於它能夠很好的利用資源來進行並行計算。確實,在 JVM 上能夠垂手可得地在後臺執行一段代碼,並在須要使用它的時候消費計算的結果。同時,它也讓開發者能夠更好的利用現代計算機硬件所帶來計算能力。html
可是,想讓計算正確並不容易,或許對於開發者最大的挑戰是編寫一個老是能運行正確的程序,而不是咱們熟悉的 「在我機器上」 是正確的。java
這篇文章會看看 Executor
裏提供的不一樣選擇。android
簡言之,Executor 是一個接口,它旨在將任務的聲明與實際計算解耦。編程
public interface Executor { void execute(Runnable command); }
它以 Runnable 實例的形式接受任務。線程會在某個時間點獲取任務並執行 Runnable::run
方法。可是,真正有難度的一般是如何選擇將要使用的 Executor 實現。在 Executors 類中已經有一些可供使用的默認實現。讓咱們來看看它們是什麼以及什麼時候選擇。api
整體上說,當選擇供後臺計算的 Executor 時,一般能夠考慮 3 個主要的問題:緩存
若是但願顯式地控制這些問題的答案,可使用 JDK 提供的靈活的 API 來建立本身的 ThreadPoolExecutor 。ThreadPoolExecutor 的構造器顯式地要求提供問題的答案:網絡
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
如下描述了這些參數的含義以及它們是如何回答以上問題的:併發
下面列出了由工廠方法 Executors 建立的不一樣 ExecutorServices 的差別。但願對在面臨如上問題並作出選擇時有幫助。oracle
newFixedThreadPool(int nThread) - n 個線程會同時進行處理,當線程池滿後,新的任務會被加入到大小沒有限制的隊列中。比較適合 CPU 密集型的任務。jvm
newWorkStealingPool(int parallelism) - 會更加所需的並行層次來動態建立和關閉線程。它一樣會試圖減小任務隊列的大小,因此比較適於高負載的環境。一樣也比較適用於當執行的任務會建立更多任務,如遞歸任務。
newSingleThreadExecutor() - 建立一個不可配置的 newFixedThreadPool(1)
,因此一個線程會執行全部的任務。它適用於明確知道可預測性以及任務須要按順序執行的狀況。
newCachedThreadPool() - 不會將任務加入隊列。能夠將它當作一個最大值爲 0 的隊列。若是當前線程都處於繁忙狀態,它會建立另一個線程來執行任務。它有時也會重用線程。它適用於防止 DOS 攻擊。緩存線程池的問題在於它不知道該合適中止建立線程。設想須要執行大量計算的任務時,若是將任務提交給 Executor ,更多的線程會消耗更多的 CPU,同時每一個任務的執行也會花更長時間。這是個多米諾效應,有更多的任務會被記錄下。這樣愈來愈多的線程會被建立,而任務的執行會更慢。很難解決這個負反饋環的問題。
因此對於大多數狀況, Executors::newFixedThreadPool(int nThreads) 是當咱們想要使用線程池時首先考慮的選擇對象。對於計算密集型的任務它一般能提供近於最優的吞吐量,對於 IO 密集型的任務也不會使任何問題變得更糟。至少若是在咱們使用這些 Executor 遇到問題並進行性能調優時,不會毫無頭緒。
固然 JVM 上有一個默認選擇的 Executor:通用的 ForkJoinPool,它是由 JVM 預設的用來並行處理流以及執行相似 CompletableFuture::supplyAsync 的任務。
聽起來很美?預設的,隨時隨地可用,最早進的線程池。還但願哪些其餘的特性?這裏有個忠告,若是有件事情聽起來太好了,那麼必定須要擦亮眼睛。ForkJoinPool 簡直太好了,除了它是通用的 common ,(即被整個 JVM 共享),它能夠被在同一 JVM 進程內的全部、任何組件使用。
若是不當心讓不合適的任務污染了它,可能會讓整個 JVM 進程受到影響。因此若是不當心讓 common 池中的工做線程阻塞,多是沒有正確地使用它。
讓咱們來看看如何讓它變得更好。ForkJoinPool 設計的初衷是爲了解決有些任務會阻塞工做線程的狀況,因此它提供了處理這種阻塞的 API 。
讓咱們歡迎 — ManagedBlocker - 能夠用它來給 ForkJoinPool 傳遞信號擴展它的並行能力,從而補償潛在可能被阻塞的工做線程。
假設咱們有一個 Call 實例,與 Retrofit 2 Call 相似,它包含全部查詢所須要的 endpoint 信息,以及如何將結果轉換成對象的信息。開始使用 Retrofit 2,儘管這篇文章主要是寫 Android ,但整體的概念與在 JVM 上使用 Retrofit 是同樣的。它提供了一套很好的 HTTP 請求的 API 。
class WS<E> implements ManagedBlocker { private final Call<E> call; volatile E item = null; public WS(Call<E> call) { this.call = call; } public boolean block() throws InterruptedException { if (item == null) item = call.execute().body(); return true; } public boolean isReleasable() { return item != null; } public E getItem() { // call after pool.managedBlock completes return item; } }
如今,當咱們想要調用 Call::execute 的時候,咱們須要保證它是經過 ForkJoinPool::managedBlock 方法進行調用的
WS ws = new WS(call); ForkJoinPool.managedBlock(ws); ws.getItem(); // obtain the result
顯然,當在 FJP 之外運行的時候它毫無心義,在線程池上運行時纔有意義。FJP 會在線程出現阻塞時生成多的工做線程。須要提醒的是,這並非銀彈,相反,頗有多是錯誤的,由於 ManagedBlocker API 是用來處理可能被阻塞的 synchronizer 對象的。這裏咱們是在處理一個阻塞網絡調用,它能夠處理當咱們查詢 4 個 urls FJP 計算資源被耗盡的狀況。
本篇文章我看了 Executors 類提供給咱們的選擇,以及什麼時候使用各個 Executor 的策略。對於 CPU 密集型的任務,newFixedThreadPool 能夠適用大多數場景,除非明確知道另一個選擇更好。可是,對於 IO 密集型的任務,並不簡單。能夠經過將 IO 調用包裝到 ManagedBlocker 裏並使用 ForkJoinPool 來加強它內部的並行能力。