天天看點

Effective Java 第三版——80. EXECUTORS, TASKS, STREAMS 優于線程

Tips

書中的源代碼位址:https://github.com/jbloch/effective-java-3e-source-code

注意,書中的有些代碼裡方法是基于Java 9 API中的,是以JDK 最好下載下傳 JDK 9以上的版本。

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]。