從淺入深掌握併發執行框架Executor

xuxh120發表於2022-02-19

引言

 

任務的執行 

大多數併發應用程式都是圍繞“任務執行(Task  Execution)”來構造的:任務通常是一些抽象的且離散的工作單元。

任務通常是一些抽象的且離散的工作單元。通過把應用程式的工作分解到多個任務中,可以簡化程式的組織結構,提供一種自然的事務邊界來優化錯誤恢復過程,以及提供一種自然的並行工作結構來提升併發性。

 


 

一、線上程中執行任務

當圍繞“任務執行”來設計應用程式時,第一步是要找出清晰的任務邊界。

在理想情況下,各個任務之間是相互獨立的:任務不依賴其他任務的狀態,結果或邊界效應。

獨立性有助於實現併發,因為如果存在足夠的處理資源,那麼這些獨立的任務都可以並行的執行。

 

在正常的負載下,伺服器應用程式應該同時表現出良好的吞吐量和快速的響應性。應用程式提供商希望程式盡支援可能多的使用者,從而降低每個使用者的服務成本,而使用者則希望獲得儘可能快的響應。而且,當負荷過載時,應用程式的效能應該是逐漸降低,而不是直接失敗。要實現上述目標,應該選擇清晰的任務邊界以及明確的任務執行策略。

 

大多數伺服器應用程式都提供了一種自然的任務邊界選擇方式:以獨立的客戶請求為邊界。比如Web伺服器,郵件伺服器,檔案伺服器,EJB容器以及資料庫伺服器等。

 

1.1 序列執行任務

 

應用程式中可以通過多種策略來排程任務,其中最簡單的策略就是在單個執行緒中序列執行各項任務。

這是最經典的一個最簡單的Socket server的例子,伺服器的資源利用率非常低,因為單執行緒在等待I/O操作完成時,CPU處於空閒狀態。從而阻塞了當前請求的延遲,還徹底阻止了其他等待中的請求被處理。

程式碼 6-1 序列的Web伺服器
public class SingleThreadWebServer {

    public static void main(String[] args) throws IOException {

        ServerSocket socket = new ServerSocket(80);

        while (true) {

            Socket connection = socket.accept();

            handleRequest(connection);

        }

    }

 

    private static void handleRequest(Socket connection) {

        // request-handling logic here

    }

}

序列的處理機制通常無法提供高吞吐率或者快速響應性。不過也有一種例外,例如當任務數量很少且執行時間很長時,或者當伺服器只為單個使用者提供服務,並且客戶每次只發出一個請求時。但大多數伺服器並不是按照這種方式來工作的。

 

1.2 顯示的建立的執行緒任務

通過為每個請求建立一個新的執行緒來提供服務,從而實現更高的響應性。如下所示:

程式碼 6-2 Web伺服器為每個請求啟動一個執行緒

class ThreadPerTaskWebServer{

    public stati void main(String[] args) throws IOException{

        ServerSocket socket = new ServerSocket(80);

        while (true){

            final Socket connection = socket.accept();

            Runnable task = new Runnable(){

                public void run(){

                    handleRequest(connection);

                }

            };


            new Thread(task).start();
        }

    }

}

只要請求的到達速率不超出伺服器的處理能力,這種方法可以同時帶來更快的響應性和更高的吞吐率。

 

1.3 無限制建立執行緒的不足

在生產環境中,“為每個任務分配一個執行緒”的做法存在一定缺陷,尤其在需要大量執行緒的場景:

  • 執行緒生命週期的開銷非常高。

執行緒的建立過程需要時間,延遲處理的請求,並且需要JVM和作業系統提供一些輔助幫助。如果請求的到達率非常高且請求的處理過程是輕量級的,那麼為每個請求建立一個新執行緒會消耗大量的計算資源。

  • 資源消耗。

活躍的執行緒會消耗系統資源,尤其是記憶體。如果已經擁有足夠的多的執行緒使CPU保持忙碌狀態,那麼在建立更多的執行緒反而會降低效能。同時會有些執行緒將閒置,大量的空閒執行緒,那麼會佔用許多記憶體,給垃圾回收器帶來壓力。

  • 穩定性。

可建立執行緒的數量存在一個限制。這個限制值將隨著平臺的不同而不同。如果超過了這些限制,那麼很可能丟擲OutOfMemoryError異常。

 

 

 


 

二、Executor框架

任務是一組邏輯工作單元,而執行緒則是使任務非同步執行的機制。為每個任務分配一個執行緒的執行策略,不僅存在諸多不足,同時資源的管理也比較複雜。執行緒池簡化了執行緒的管理工作,並且java.util.concurrent提供了一種靈活的執行緒池作為Executor框架的一部分。在Java類庫中,任務執行的主要抽象不是THread而是Executor。

程式碼 6-3 Executor介面

public interface Executor {

    /**

     * Executes the given command at some time in the future.  The command

     * may execute in a new thread, in a pooled thread, or in the calling

     * thread, at the discretion of the {@code Executor} implementation.

     *

     * @param command the runnable task

     * @throws RejectedExecutionException if this task cannot be

     * accepted for execution

     * @throws NullPointerException if command is null

     */
    void execute(Runnable command);

}

雖然 Executor 是個簡單的介面,但是卻為靈活且強大的非同步任務執行框架提供了基礎。該框架能支援多種不同型別的任務執行策略。它提供了一種標準的方法將任務的提交過程與執行過程解耦開來,並用Runnable來表示任務。Executor的實現還支援對生命週期的支援,以及統計資訊的收集、應用程式管理機制和效能監視等機制。

Executor基於生產者—消費者設計模式,提交任務的操作單元相當於生產者(生成待完成的工作單元),執行任務的執行緒相當於消費者(執行完這些工作單元)。

 

2.1 示例:基於ExecutorWeb伺服器

基於Executor來構建Web伺服器是非常容易的。ThreadPerTaskWebServer使用了一種標準的Executor實現,即一個固定長度的執行緒池,可以容納100個執行緒。

程式碼 6-4 基於執行緒池的Web伺服器

class ThreadPerTaskWebServer{

    //定義執行緒池大小

    private static final int NTHREAD = 100;

    //定義Executor

    private static final Executor exec = 

        Executors.newFixedThreadPool(NTHREAD);

    public static void main(String[] args) throws IOException{

        ServerSocket socket = new ServerSocket(80);

        while (true){

            final Socket connection = socket.accept();

            Runnable task = new Runnable(){

                public void run(){

                    handleRequest(connection);

                }

            };

            //將任務新增到執行緒池中

            exec.execute(task);

        }

    }

}

 

2.2 執行策略

Executor為任務提交和任務執行之間的解耦提供了標準的介面,你可以為某一類任務指定一個特定的執行策略。任務的執行策略定義了任務執行的“What、Where、When、How”,包括:

  • 任務在什麼執行緒中執行?(what?)
  • 任務以什麼順序執行?(what?)
  • 可以有多少個任務併發執行?(how many?)
  • 可以有多少個任務進入等待執行佇列?(how many?)
  • 如果系統過載,需要放棄一個任務,應該挑選哪一個任務?(which?)另外,如何通知應用程式知道這一切呢?(how?)
  • 在一個任務的執行前與結束後,應該做些什麼?(what?)

執行策略是資源管理的工具。最佳的策略取決於你的計算資源和你對服務的要求。通過控制併發任務的數量,來保證你的任務不會因為計算資源不足而失敗,也不會出現因為高併發帶來爭奪資源時的效能問題。

將任務的提交與執行解耦,還有助於在實現過程中選擇一個與當前硬體最匹配的執行策略。

 

2.3 執行緒池

執行緒池,從字面含義卡,指的是管理一組同構工作執行緒的資源池。執行緒池與工作佇列(Work Queue)密切相關有關,其中在工作佇列中儲存了所有等待執行的任務。工作者執行緒(Worker Theahd)的任務很簡單:從工作佇列中獲取一個任務,執行任務,然後返回執行緒池並等待下一個任務。

使用執行緒池有一下幾個幾個優點:

1.通過重用現有的執行緒而不是建立新執行緒,可以避免執行緒建立和銷燬的開銷。

2.當請求到達時,工作執行緒通常已經存在,減少了等待執行緒建立的時間,從而提高響應性。

3.通過調整執行緒池的大小,可以建立足夠多的執行緒以便使處理器保持忙碌狀態,同時防止過多執行緒相互競爭資源,使應用程式耗盡記憶體或者失敗,提高了應用程式穩定性。

 

類庫提供了一個靈活的執行緒池以及一些有用的預設配置。通過Executor中的靜態工廠方法之一來建立執行緒池:

a. newFixedThreadPool

    建立一個固定長度的執行緒池,每當提交一個任務時就建立一個執行緒,直到達到執行緒的最大數量,這是執行緒池的規模不再變化。如果某個執行緒由於發生了未預期的Exception而結束,那麼執行緒池會補充一個新的執行緒。

b. newCachedThreadPool

    建立一個可快取的執行緒池,如果執行緒池的當前規模超過了處理需求,那麼會回收空閒的執行緒,而當需求增加時,則可以新增新的執行緒,執行緒池的規模不存在任何限制。

c. newSingleThreadPool

    是一個蛋執行緒Executor,她建立單個工作者執行緒執行任務。如果這個任務異常結束,則會建立另一個執行緒來代替。newSingleThreadPool確保依照任務在工作佇列中的順序來序列執行。

d. newScheduledThreadPool

    建立一個固定長度的執行緒池,而且以延遲或定時的方式來執行任務,類似於Timer。

 

“為每個任務分配一個執行緒”策略變成基於執行緒池的策略,將對應用程式的穩定性產生重大的影響。Web伺服器不會在高負載的情況下失敗。也不會建立數千個執行緒來爭奪有限的CPU和記憶體資源,因此伺服器效能將平緩的降低。

使用Executor還可以實現各種調優,管理、監視、記錄日誌、錯誤報告和其他功能,如果不使用任務執行框架,那麼要增加這些功能是非常困難的。

 

2.4 Executor的生命週期

我們已經知道如何建立一個Executor,但並沒有討論如何關閉它。Executor的實現通常會建立執行緒來執行任務,但JVM只有在所有(非守護執行緒)執行緒全部終止後才會退出。因此,如果無法正確地關閉Executor,那麼JVM將無法結束。

既然Executor是為應用程式提供服務的,因而他們也是可關閉的,無論採用平緩的當時,還是粗暴的方式,並將在關閉操作中受影響的任務的狀態反饋給應用程式。

ps:平緩的關閉形式:完成所有已經啟動的任務,並且不再接受任何新的任務。粗暴的關閉形式:直接關掉機房的電源。

為了解決執行服務的生命週期問題,Executor擴充套件來ExecutorService介面,新增了一些用於生命週期管理的方法:

程式碼 6-7 ExecutorService中的生命週期管理方法
public interface ExecutorService extends Executor{

    void shutdown();

    List<Runnable> shutdownNow();

    boolean isShutdown();

    boolean isTerminated();

    boolean awaitTerminated(long timeout, TimeUtil unit)
        throws InterruptedException;

}

Executor有三種狀態:執行(running)、關閉(shutting down) 終止(terminated)

ExecutorService最初建立的時候是執行狀態的。呼叫shutdown方法後,會啟動一個平緩的關閉過程:停止接收新的任務,同時等待已經提交的任務完成--包括還未開始執行的任務。呼叫shutdownNow方法會啟動一個強制關閉的過程:嘗試取消所有執行中的任務和排列在佇列中尚未開始執行的任務。

 

在Executor關閉後提交的任務將由“拒絕執行處理器處理(Rejected Execution Handler)”來處理。

它會拋棄任務,或者是的execute方法丟擲一個未檢查的RejectedExecutionException

等所有任務都執行完成後,ExecutorService會進入終止狀態;可以呼叫awaitTermination方法等待ExecutorService到達終止狀態,也可以使用isTerminated方法輪詢來ExecutorService是否已經終止。 

通常在呼叫 awaitTermination 方法之後會立即呼叫shutdown,從而產生同步地關閉ExecutorService效果。

 

程式碼 6-8 LifecycleWebServer通過增加生命週期支援來擴充套件web服務的功能。

 

程式碼 6-8 支援關閉操作的Web伺服器
class LifecycleWebServer{

    private final ExecutorService exec = ...;

    public static void main(String[] args) throws IOException{
        ServerSocket socket = new ServerSocket(80);

        while (! exec.isShutdown()){
            try{

                final Socket connection = socket.accept();

                exec.execute(new Runnable(){
                    public void run() { handleRequest(connection); }
                })

            } catch (RejectedExecutionException e){
                if (!exec.isShutdown())
                    log("task submission rejected",e);

            }

        }

    }

    public void stop() { exec.shutdown(); }

    void handleRequest(Socket connection){

        Request req = readRequest(connection);

        //判斷是否為請求關閉的指令

        if (isShutdownRequest(req))
            stop();

        else 
            dispatchRequest(req);

    }

}

 

2.5 延遲任務與週期任務

使用Timer的弊端在於

  • 如果某個任務執行時間過長,那麼將破壞其他TimerTask的定時精確性(執行所有定時任務時只會建立一個執行緒),只支援基於絕對時間的排程機制,所以對系統時鐘變化敏感
  • TimerTask丟擲未檢查異常後就會終止定時執行緒(不會捕獲異常)

Java5.0或者更高版本的JDK中,將很少使用Timer。

如果要構建自己的排程服務,那麼可以使用DelayQueue。它實現了BlockingQueue,併為ScheduledThreadPoolExecutor提供排程功能。在DelayQueue中,只有某個元素逾期後才能從DelayQueue中執行take操作。從DelayQueue中返回的物件將根據他們的延遲時間排序。

 

 


 

三、找出可利用的並行性

Executor框架幫助指定執行策略,但如果要使用Executor,必須將任務表述為一個Runnable。在大多數的伺服器應用程式中都存在一個明顯任務邊界:單個客戶請求。但有時候任務的邊界並非是顯而易見的,例如在很多桌面應用程式中,即使是伺服器應用程式在單個客戶請求中仍可能存在可發掘的並行性,例如資料庫伺服器。

本節中我們將開發一些不同版本的元件,並且每個版本都實現了不同程度的併發性。該示例元件實現瀏覽器程式中的頁面渲染(Page-Rendering)功能,它的作用是將HTML頁面繪製到影像快取中。為了簡便,假設HTML頁面只包含標籤文字,以及預定大小的圖片和URL。

 

3.1 示例:序列的頁面渲染器

最簡單的方法就是對HTM L文件進行序列處理。先繪製文字元素,同時為影像預留出矩形的站位空間,在處理完了第一遍文字後,程式在開始下載影像並繪製到相應的佔位空間中。如下:

 

程式碼 6-10 序列地渲染頁面元素

public class SingleThreadRenderer{

    void renderPage(CharSequence source){

        //載入文字
        renderText(source);

        List<ImageData> ImageData = new ArrayList<ImageData>();

        //下載圖片
        for (ImageData imageInfo : scanForImageInfo(source))
            ImageData.add(imageInfo.downloadImage());

        //載入圖片
        for (ImageData data : ImageData)
            renderImage(data);

    }

}

影像下載過程的大部分時間都是在等待IO操作執行完成,在這期間CPU幾乎不做任何工作。因此,在這種序列執行方法中沒有充分地利用CPU,是的頁面的家在時間較長。通過將問題分解為多個獨立的任務併發執行,能夠獲得更高的CPU利用率和響應靈敏度。

 

 

3.2 攜帶結果的任務 CallableFuture

 

Executor框架使用Runnable作為其基本的任務表示形式。Runnable是一種有很大侷限的抽象,雖然run能寫入到日誌檔案或者將結果放入某個共享的資料結構,但它不能返回一個值或者丟擲一個受檢查的異常。

 

許多工實際上都是存在延遲的計算:計算某個複雜的功能或者執行資料庫查詢,從網路上獲取資源。對於這些任務,Callable是一種更好的抽象:它認為主入口點(即call)將返回一個值,並可能丟擲一個異常。

Runnalbe和Callable描述的都是抽象的計算任務。這些任務通常是有範圍的,即都有一個明確的起始點,並且最終會結束。Executor執行的任務有4個生命週期階段:建立、提交、開始和完成。有時希望取消某些任務的執行。

程式碼 6-11 Callable與Future介面
public interface Callable<V> {

    V call() throws Exception;

}


public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException,ExecutionException,
            CancellationException;

    V get(long timeout, TimeUtil unit) 
        throws InterruptedException,ExecutionException, 
                  CancellationException, TimeoutException;

}

Future表示了一個任務的生命週期,並提供了相應的方法判斷是否完成或被取消以及獲取執行結果,以及獲取任務的結構和取消任務等。

get方法的行為取決於任務的狀態:尚未開始,正在執行,已完成。

  •     如果任務已經完成,那麼將立即返回或丟擲一個 Exception;
  •     如果任務沒有完成,那麼get將阻塞,直到任務完成;
  •     如果任務丟擲了異常,那麼個get將該異常封裝為 ExecutionException並重新丟擲;
  •     如果任務被取消,那麼get將丟擲CancellationException,同時還可以通過getCause來獲得被封裝的初始異常;

 

可以通過許多種方法建立一個Future來描述任務:

  • ExecutorService中的所有submit方法都將返回一個Future,從而將一個Runnable或Callable提交給Executor
  • 還可以顯示的為某個指定的Runnable或者Callable實力化一個 FutureTask。FutureTask實現了Runnable,因此可以將它提交給Executor來執行,或者直接呼叫其run方法。
  • 從Java 6開始,ExecutorService實現可以改寫AbstractExecutorService中的newTaskFor方法,從而根據已經提交的Runnable或Callable來控制Future的例項化過程。在預設實現中僅建立了一個新的FutureTask,程式碼:
程式碼 6-12 AbstractExecutorService中newTaskFor的預設實現
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {

    return new FutureTask<T>(runnable, value);

}

在將Runnable或Callable提交到Executor到Executor的過程中,包含了一個安全釋出,即將任務從提交執行緒釋出到最終的執行執行緒。類似的,在設定Future結果的過程中也包含一個安全的釋出,即將這個結果從執行執行緒釋出到任何通過get方法獲得它的執行緒。

 

3.3 示例:使用Future 實現頁面渲染器

為了使頁面渲染器實現更高的併發性,將渲染的過程分解為兩個任務,一個是渲染所有的文字(CPU密集型),另一個是下載所有的影像(IO密集型)。

程式碼 6-13 FutureRenderer
public class FutureRenderer {

    private final ExecutorService exec = ....;

    void renderPage(CharSequence source){

        final List<ImageInfo> imageInfos = scanForImageInfo(source);

        Callable<List<ImageData>> task = 

            new Callable<List<ImageData>>() {

                public List<ImageData> call(){

                    public List<ImageData> result = new ArrayList<ImageData>();

                    for (ImageInfo imageInfo : imageInfos)
                        result.add(imageInfo.downloadImage());

                    return result;

                }

            };

        Future<List<ImageData>> future = exec.submit(task);
        renderText(source);

        try{
            List<ImageData> imageData = future.get();

            for (ImageData data : imageData)
                renderImage(data);

        } catch (InterruptedException e){
            //重新設定執行緒的中斷狀態
            Thread.currentThread().interrupt();

            //由於不需要結構,因此取消任務
            future.cancel(true);

        } catch (ExecutionException e){
            throw launderThrowable(e.getCause());

        }

    }
}

Get方法擁有“狀態依賴”的內在特性,因而呼叫者不需要知道任務的狀態,此外在任務提交和獲得結果中包含的安全釋出屬性也確保了這個方法是執行緒安全的。

 

3.4 在異構任務並行化中存在的侷限

在上面的FutureRender中使用了兩個任務,一個是負責渲染文字,一個是負責渲染圖片。如果渲染文字的速度遠遠高於渲染圖片的速度,那麼程式的最終效能與序列執行的效能差別並不大,而程式碼卻變複雜了。

然而,通過對異構任務進行並行化來獲得很大的效能提升是困難的。只有當大量相互獨立且同構的任務可以併發進行處理時,才能體現出將程式的工作負載分配到多個任務中帶來的真正的效能提升。

 

3.5 CompletionService:Executor BlockingQueue

如果向Executor提交了一組計算任務,並且希望在計算完成後獲得結果,那麼可以保留與每個任務關聯的Future,然後反覆使用get方法,同時將引數timeout指定為0,從而通過輪詢來判斷任務是否完成。這種方法雖然可行,但卻有些繁瑣。幸運的是,還有一種更好的方法:CompletionService 完成服務。

 

CompletionService將Executor和BlockingQueue的功能融合在一起。可以通過將Callable任務提交給她來執行,然後使用類似於佇列操作的take和poll等方法來獲得已完成的結果,而這些結果也會在完成時將被封裝為Future。ExecutorCompletionService實現了CompletionService,並將計算部分委託給一個Executor

 

3.6 示例:使用 CompletionService 實現頁面渲染

通過CompletionService從兩個方面來提高頁面渲染器的效能:縮短總執行時間以及提高響應性。

為每一幅影像的下載都建立一個獨立任務,並線上程池中執行他們,從而將序列的下載過程轉換為並行的過程,這將減少下載所有影像的總時間。

 

程式碼 6-15 使用CompletionService,使頁面在下載完成後立即顯示出來:

程式碼 6-15 使用CompletionService,使頁面在下載完成後立即顯示出來

public class Renderer {

      private final ExecutorService executor;

      Renderer(ExecutorService executor){ this.executor=executor; }

      void renderPage(CharSequence source){

          List<ImageInfo> info=scanForImageInfo(source);

          //ExecutorCompletionService實現了CompletionService,並將計算部分委託給一個Executor。

          CompletionService<ImageData> completionService=

                  new ExecutorCompletionService<ImageData>(executor);

          for(final ImageInfo imageInfo:info)

              completionService.submit(new Callable<ImageData>(){

                 public ImageData call(){

                     return imageInfo.downloadImage(); //CompletionService中有類似佇列的操作

                 }

              });

          renderText(source);

          try{

              for(int t=0,n=info.size();t<n;t++){

                  Future<ImageData> f = completionService.take();

                  ImageData imageData=f.get();

                  renderImage(imageData);

              }

          }catch (InterruptedException e) {
            Thread.currentThread().interrupt();

        }catch (ExecutionException e) {
            throw LaunderThrowable.launderThrowable(e.getCause());

        }

      }

}

 

3.7 為任務設定時限

有時候,如果某個任務無法在指定時間內完成,那麼將不再需要它的結果,此時可以放棄這個任務。

例如,某個Web應用程式從外部的廣告伺服器上獲取廣告資訊,但如果應用程式在2秒內得不到響應,那麼將顯示一個預設的廣告,這樣即使不能獲得廣告資訊,也不會降低站點的響應效能。

 

在有限時間內執行任務的主要困難在於,要確保得到的答案的時間不會超過限定的時間,或者在限定的時間內無法獲得答案。在支援時間限制的Future.get中支援這種需求:當結果可用時,它立即返回,如果在指定時限內沒有計算出結果,那麼將丟擲TimeoutException。

 

在使用時限任務時需要注意,當這些任務超時後應該立即停止,從而避免為繼續計算一個不再使用的結果而浪費計算資源。因此在get方法丟擲TimeoutException時,可以通過Future來取消任務。

 

程式碼 6-16   在指定時間內獲取廣告資訊
public class RenderWithTimeBudget {

     private static final Ad DEFAULT_AD = new Ad();
     private static final long TIME_BUDGET = 1000;
     private static final ExecutorService exec = Executors.newCachedThreadPool();

     Page renderPageWithAd() throws InterruptedException{

         long endNanos=System.nanoTime()+TIME_BUDGET; //返回納秒級的時間,再加上時限
         Future<Ad> f=exec.submit(new FetchAdTask());

         //在等待廣告的同時顯示頁面
         Page page=renderPageBody();
         Ad ad;

         try{
            //只等待指定的時間長度
            long timeLeft=endNanos-System.nanoTime();
            ad = f.get(timeLeft, NANOSECONDS);//在指定時限內獲取,NANOSECONDS為時間單位

         }catch (ExecutionException e) {
            ad = DEFAULT_AD;

        }catch (TimeoutException e) {  
            //如果超時了,廣告轉為預設廣告,並取消獲取任務
            ad = DEFAULT_AD;
            f.cancel(true);
        }

         page.setAd(ad);
         return page;

     }

}

 

3.8 示例:旅行預訂入口網站

“預訂時間”方法可以很容易地擴充套件到任意數量的任務上。考慮這樣一個旅行預訂入口網站:使用者輸入旅行的日期和其他要求,入口網站獲取並顯示來自多條航線、旅店和汽車租賃公司的報價。在獲取不同公司報價的過程中,可能會呼叫Web服務、訪問資料庫、制定一個EDI事物或者其他機制。在這種情況下,不宜讓頁面的響應時間受限於最慢的響應時間,而應該只顯示指定時間內收到的資訊。對於沒有及時響應的服務提供者,頁面可以忽略他們,或者其他操作。

 

從一個公司獲取報價的過程與其他公司獲得報價的過程無關,因此可以將獲取報價的過程當成一個任務,從而使獲得報價的過程能併發執行。建立n個任務,將其提交到執行緒池,保留n個Futrue,並使用限時的get方法通過Future序列地獲取每一個結果,這一切都很簡單,但我們還可以使用一個更簡單的方法——invokeAll。

程式碼 6-17     在預定時間內請求旅遊報價
private class QuoteTask implements Callable<TravelQuote>{

    private final TravelCompany company;
    private final TravelInfo travelInfo;

    public TravelQuote call()throw Exception{
        return company.solicitQuote(travelInfo);
    }

}

 

public List<TravelQuote> getRankedTravelQuotes(
      TravelInfo travelInfo, Set<TravelCompany> companies,
      Comparator<TravelQuote> ranking(long time, TimeUnit unit)

      throws InterruptedException {
             List<QuoteTask> tasks = new ArrayList<QuoteTask>();

             //為每家公司新增報價任務
             for (TravelCompany company : companies)
                  tasks.add(new QuoteTask(company, travelInfo));

             //InvokeAll方法的引數為一組任務,並返回一組Future ,用時限來限制時間       
             List<Future<TravelQuote>> futures = exec.invokeAll(tasks, time, unit);

 

          List<TravelQuote> quotes = new ArrayList<TravelQuote>(tasks.size());
          Iterator<QuoteTask> taskIter = tasks.iterator();

          for (Future<TravelQuote> f : futures) {
             QuoteTask task = taskIter.next();

             try {
                //invokeAll按照任務集合中迭代器額順序肩所有的Future新增到返回的集合中
                quotes.add(f.get());

             } catch (ExecutionException e) {
                quotes.add(task.getFailureQuote(e.getCause()));

             } catch (CancellationException e) {
                quotes.add(task.getTimeoutQuote(e));

             }

         }

         Collections.sort(quotes, ranking);
          return quotes;

}

在程式6-17中使用了支援時限的inokAll,將多個任務提交到一個ExecutorService並獲得結果。

invokeAll方法有以下幾個特性:

  • invokeAll方法的引數為一組任務,並返回一組Future。這兩個集合有著相同的結構。invokeAll按照任務集合中迭代器的順訊將所有的Future新增到返回的集合中,從而使呼叫者能將各個Future與其表示的Callable關聯起來。
  • 當所有任務都執行完畢時,或者呼叫執行緒被中斷時,又或者超過指定時限時,ivokeAll將返回。
  • 當超過指定時限後,任何還未完成的任務都會取消。
  • invokeAll返回後,每個任務要麼正常地完成,要麼被取消,而客戶端程式碼可以呼叫get或者isCancelled來判斷究竟是何種情況。

 


 

四、小結

  • 通過圍繞任務執行來設計應用程式,可以簡化開發的過程,並有助於實現併發。
  • Executor框架將任務提交與執行策略解耦開來,同時還支援多種不同型別的執行策略。
  • 想要在將應用程式分別為不同的任務時獲得最大的好處,必須定義清晰的任務邊界。

 

 

相關文章