ExecutorService
接口及相關API細節詳解。
Java Magazine上面有一個專門坑人的面試題系列: https://blogs.oracle.com/javamagazine/quiz-2。
這些問題的設計宗旨,主要是測試面試者對Java語言的瞭解程度,而不是爲了用彎彎繞繞的手段把面試者搞蒙。
如果你看過往期的問題,就會發現每一個都不簡單。
這些試題模擬了認證考試中的一些難題。 而 “中級(intermediate)” 和 “高級(advanced)” 指的是試題難度,而不是說這些知識本身很深。 一般來說,“高級”問題會稍微難一點。
問題(高級難度)
此問題的目的是考察如何通過 Runnable
和 Callable
來創建任務,並使用 ExecutorService
來併發執行。
我們有一個 Logger
類,定義如下所示:
class Logger implements Runnable {
String msg;
public Logger(String msg) {
this.msg = msg;
}
public void run() {
System.out.print(msg);
}
}
並給出如下使用的代碼:
Stream<Logger> s = Stream.of(
new Logger("Error "),
new Logger("Warning "),
new Logger("Debug "));
ExecutorService es =
Executors.newCachedThreadPool();
s.sequential().forEach(l -> es.execute(l));
es.shutdown();
es.awaitTermination(10, TimeUnit.SECONDS);
這裏省略了相關的 import
語句, 假設代碼能編譯並正常啓動。 請選擇兩項可能的輸出結果:
- A、 Error Debug Warning
- B、 Error Warning Debug
- C、 Error Error Debug
- D、 Error Debug
答案和解析
這道試題屬於 Executors
類和 ExecutorService
接口相關的考點,順帶考察 Executors
工具類自帶的 ExecutorService
線程池實現。
在Java的早期版本中,需要程序員手工創建和管理線程。線程是系統內核級的重要資源,並不能無限創建; 而且創建線程的開銷很大,所以開發中一般會使用資源池模式,也就是創建 “線程池”。通過線程池,可以用少量的線程,來執行大量的任務。
線程池的思路是這樣的: 與其爲每個任務創建一個線程,執行完就銷燬; 倒不如統一創建少量的線程, 然後將任務邏輯用 Runnable
包裝起來, 提交給線程池來調度執行。
有任務需要調度的時候,線程池找一個空閒的線程,並通知他幹活。 任務執行完成後,再將這個線程放回池子裏,等待下一次調度。
Java 5.0 開始提供標準的線程池API。 通過 Executor
和 ExecutorService
接口定義了線程池以及支持的交互操作。
另外,我們可以使用 Executors
的靜態工廠方法來實例化 ExecutorService
的各種實現。
相關的基礎類和接口都位於 java.util.concurrent
包中, 在編寫簡單的併發任務時,可以直接使用。
Executor
是頂層接口, 定義了執行 Runnable
任務的方法;但我們一般用的是子接口 ExecutorService
及其實現。
ExecutorService
接口中增加了處理 Callable
的方法, 以及關閉線程池的功能。
實現 Callable
接口的任務會返回一個結果, 調用方可以通過提交任務時返回的 Future
對象,來異步獲取任務的執行狀態和結果,這樣就對任務有了一定的管理和控制能力。
Executor
和 ExecutorService
接口並沒有規定使用哪種調度策略來執行。
- 有些線程池,使用固定數量的線程來併發地執行任務,新提交的任務要等到有空閒線程纔會被執行。
- 有的線程池, 在工作負載上升時自動增加線程,並在需求降低時清理掉一部分線程。
- 還有的線程池只使用單個線程,直接按順序執行提交的任務。
這些特徵取決於具體的實現,需要開發者根據業務系統的特徵來權衡,並選擇適當的線程池。 針對這幾類線程池,Executors
工具類提供了三種工廠方法:
newFixedThreadPool
newCachedThreadPool
newSingleThreadExecutor
前兩個方法創建的線程池可以有多個worker線程, 而 newSingleThreadExecutor
方法創建的線程池則只有單個線程。
回到前面的問題, 試題中給出的代碼創建了緩存模式的線程池。
這類線程池會根據需要生成新的worker線程,並清理一段時間內沒有使用到的線程。
但緩存模式的線程池有一個嚴重缺點: 創建的線程數有可能不被限制, 那樣的話會導致大量的資源佔用。 在高負載場景下,可能會由於資源爭用而導致性能急劇下降。
因爲創建的線程池具有多個線程, 所以後面提交的任務可以併發執行。
無論誰先開始,我們都無法對其執行進度做出精確預測。
也就是說,他們輸出消息的順序可能是任意的。
由此得知, 選項A
和 選項B
都 正確
。
ExecutorService
會保證提交的任務最多被執行一次。
在某些情況下,任務可能不會執行,或者在執行完成之前線程池就被關閉了。
因爲具有最多執行一次的特徵,所以我們不會看到任何重複的消息。因此可以判斷,選項C不正確
。
在調用 shutdown
方法之後,ExecutorService
會拒絕新的任務提交請求, 但已有的任務會繼續運行,直到所有的作業全部執行完纔會關閉。
因此在這裏給的代碼中, 三個消息都會看到。 因此可知,選項D不正確
。
順便提一句,可能有些讀者會認爲,如果在10秒內執行不完, 那麼選項D也可能是正確的。
但反過來說,如何確定這個消息會被打印呢?
因爲試題中給出的任務邏輯非常簡單,很明顯不可能10秒鐘還執行不完。
而且我們通過分析能判斷出 選項A
和 選項B
是正確的, 那麼做題時就可以將這種不可能的情況排除。
當然,你可能對選項D感興趣,因爲在其他某些極端的情況下, 作業無法在10秒內完成,比如恰好在這個時刻操作系統啓動升級或更新。
請注意,在給定的代碼中,沒有任何證據表明 JVM 將被強行關閉。
而且默認創建的線程都是 非守護線程
(nondaemon thread),因此,在作業完成之前,JVM 不會退出。
所以,如果允許程序運行,則對應的消息都會被打印出來。
總結
正確的選項是 A
和 B
。
相關鏈接
原文鏈接: https://blogs.oracle.com/javamagazine/quiz-advanced-executor-service