Java坑人面試題系列: 線程/線程池(高級難度)

ExecutorService 接口及相關API細節詳解。

Java Magazine上面有一個專門坑人的面試題系列: https://blogs.oracle.com/javamagazine/quiz-2

這些問題的設計宗旨,主要是測試面試者對Java語言的瞭解程度,而不是爲了用彎彎繞繞的手段把面試者搞蒙。

如果你看過往期的問題,就會發現每一個都不簡單。

這些試題模擬了認證考試中的一些難題。 而 “中級(intermediate)” 和 “高級(advanced)” 指的是試題難度,而不是說這些知識本身很深。 一般來說,“高級”問題會稍微難一點。

問題(高級難度)

此問題的目的是考察如何通過 RunnableCallable 來創建任務,並使用 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。 通過 ExecutorExecutorService 接口定義了線程池以及支持的交互操作。
另外,我們可以使用 Executors 的靜態工廠方法來實例化 ExecutorService 的各種實現。
相關的基礎類和接口都位於 java.util.concurrent 包中, 在編寫簡單的併發任務時,可以直接使用。

Executor 是頂層接口, 定義了執行 Runnable 任務的方法;但我們一般用的是子接口 ExecutorService 及其實現。
ExecutorService 接口中增加了處理 Callable 的方法, 以及關閉線程池的功能。
實現 Callable 接口的任務會返回一個結果, 調用方可以通過提交任務時返回的 Future 對象,來異步獲取任務的執行狀態和結果,這樣就對任務有了一定的管理和控制能力。

ExecutorExecutorService 接口並沒有規定使用哪種調度策略來執行。

  • 有些線程池,使用固定數量的線程來併發地執行任務,新提交的任務要等到有空閒線程纔會被執行。
  • 有的線程池, 在工作負載上升時自動增加線程,並在需求降低時清理掉一部分線程。
  • 還有的線程池只使用單個線程,直接按順序執行提交的任務。

這些特徵取決於具體的實現,需要開發者根據業務系統的特徵來權衡,並選擇適當的線程池。 針對這幾類線程池,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 不會退出。

所以,如果允許程序運行,則對應的消息都會被打印出來。

總結

正確的選項是 AB

相關鏈接

原文鏈接: https://blogs.oracle.com/javamagazine/quiz-advanced-executor-service

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章