Effective Java 第三版——80. EXECUTORS, TASKS, STREAMS 優於執行緒

林本託發表於2019-04-01

Tips
書中的原始碼地址:https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些程式碼裡方法是基於Java 9 API中的,所以JDK 最好下載 JDK 9以上的版本。

Effective Java, Third Edition

80. EXECUTORS, TASKS, STREAMS 優於執行緒

本書的第一版包含一個簡單工作佇列的程式碼[Bloch01,條目 49]。 此類允許客戶端將後臺執行緒的非同步處理工作排入佇列。 當不再需要工作佇列時,客戶端可以呼叫一個方法,要求後臺執行緒在完成佇列中已有的任何工作後正常終止自身。 實現只不過是個玩具,但即便如此,它還需要一整頁精細,細緻的程式碼,如果你沒有恰到好處的話,這種程式碼很容易出現安全和活性失敗。 幸運的是,沒有理由再編寫這種程式碼了。

到本書第二版出版時,java.util.concurrent包已新增到Java中。 該包包含一個Executor Framework,它是一個靈活的基於介面的任務執行工具。 建立一個比本書第一版更好的工作佇列只需要一行程式碼:

ExecutorService exec = Executors.newSingleThreadExecutor();

下面是如何提交一個可執行的(runnable)執行:

exec.execute(runnable);

下面是如何告訴executor優雅地終止(如果做不到這一點,你的虛擬機器很可能不會退出):

exec.shutdown();

可以使用執行器服務(executor service)做更多的事情。例如,可以等待一個特定任務完成(條目 79中使用get方法, 319頁),可以等待任何或全部任務完成的集合(使用invokeAny或invokeAll方法),也可以等待執行者服務終止(使用awaitTermination方法),可以在完成任務時逐個檢索任務結果(使用ExecutorCompletionService),可以安排任務在特定時間執行或定期執行(使用ScheduledThreadPoolExecutor),等等。

如果希望多個執行緒處理來自佇列的請求,只需呼叫另一個靜態工廠,該工廠建立一種稱為執行緒池的不同型別的執行器服務。 可以建立具有固定或可變數量執行緒的執行緒池。 java.util.concurrent.Executors類包含靜態工廠,它們提供了你需要的大多數執行程式。 但是,如果想要一些與眾不同的東西,可以直接使用ThreadPoolExecutor類。 此類允許你配置執行緒池操作的幾乎每個方面。

為特定應用程式選擇執行程式服務可能很棘手。 對於小程式或負載較輕的伺服器,Executors.newCachedThreadPool通常是一個不錯的選擇,因為它不需要配置,通常“做正確的事情”。但是對於負載很重的生產伺服器來說,快取執行緒池不是一個好的選擇! 在快取執行緒池中,提交的任務不會排隊,而是立即傳遞給執行緒執行。 如果沒有可用的執行緒,則建立一個新執行緒。 如果伺服器負載過重以至於所有CPU都被充分利用並且更多工到達時,則會建立更多執行緒,這隻會使事情變得更糟。 因此,在負載很重的生產伺服器中,最好使用Executors.newFixedThreadPool,它提供具有固定執行緒數的池,或直接使用ThreadPoolExecutor類,以實現最大程度的控制。

不僅應該避免編寫自己的工作佇列,而且通常應該避免直接使用執行緒。 當直接使用Thread類時,執行緒既可以作為工作單元,也可以作為執行它的機制。 在executor framework中,工作單元和執行機制是分開的。 關鍵的抽象是工作單元,稱為任務。 有兩種任務:Runnable及其近親Callable(類似於Runnable,除了它返回一個值並且可以丟擲任意異常)。 執行任務的一般機制是executor service。 如果從任務的角度來看,讓executor service為你執行它們,可以靈活地選擇適當的執行策略以滿足你的需求,並在需求發生變化時更改策略。 本質上本質上,Executor Framework執行的功能與Collections Framework聚合(aggregation)功能是相同的。

在Java 7中,Executor Framework被擴充套件為支援fork-join任務,這些任務由稱為fork-join池的特殊executor service執行。 由ForkJoinTask例項表示的fork-join任務可以拆分為較小的子任務,而包含ForkJoinPool的執行緒不僅處理這些任務,而且還“彼此”竊取“任務”以確保所有執行緒都保持忙碌,從而導致更高的任務 CPU利用率,更高的吞吐量和更低的延遲。 編寫和調優fork-join任務很棘手。 並行流(Parallel streams)(條目 48)是在fork-join池之上編寫的,假設它們適合當前的任務,那麼你可以輕鬆地利用它們的效能優勢。

對Executor Framework的完整處理超出了本書的範圍,但感興趣的讀者可以參考 《Java Concurrency in Practice》一書[Goetz06]。

相關文章