1. 程式人生 > >[Java併發程式設計實戰] Executor框架(含思維導圖)

[Java併發程式設計實戰] Executor框架(含思維導圖)

亦餘心之所善兮,雖九死其猶未悔。———屈原《離騷》
這句話的意思是“這些都是我內心之所珍愛,就是讓我九死(或多死)還是不後悔。”這兩句表現了詩人對美好理想執著追求的精神。

PS: 如果覺得本文有用的話,請幫忙點贊,留言評論支援一下哦,您的支援是我最大的動力!謝謝啦~

這篇文章介紹 Executor 框架,我用 Xmind 軟體畫了這篇文章內容的思維導圖。這是我第一次用思維導圖軟體,發現真的很好用,將整個文章提取然後視覺化,複習起來也一目瞭然。話不所說,誰用誰知道。下面放上這篇文章的思維導圖:

這裡寫圖片描述

Executor 簡介

從程式碼上看,Executor 是一個簡單的介面,但它卻是整個非同步任務執行框架的基礎,這個框架能支援多種不同型別的任務執行策略。他提供了一種標準的方法將任務的提交過程和執行過程解耦開來,任務用 Runnable 來表示。Executor 基於生產者-消費者模式,提交任務的執行緒相當於生產者,執行任務的執行緒相當於消費者。同時,Executor 的實現還提供了對任務執行的生命週期管理的支援。

Executor 引入的原因

大多數併發應用程式都是圍繞[任務執行]來構造,應用程式的工作可以被分解到多個任務中,關鍵是如何安排好這些任務的執行。換句話說,就是有哪些執行策略可以供我們選擇,來保證程式的正常執行。執行策略有下面三種方法,我們來看看。

序列執行

最簡單最直接最粗暴的方法就是單執行緒的序列執行,每次只能執行一個任務,其他任務必須等待當前任務執行完成,才能進行。這在理論上可以的,但是它的缺陷也非常明顯。假如當前任務是耗時操作或者是阻塞的,那麼其他任務需要進行長時間的等待,這讓客戶端感覺到程式響應非常慢。特別是在伺服器應用程式中,序列處理機制通常無法提供高吞吐率或快速響應性。它的缺陷總結如下:

  • 吞吐率低
  • 響應慢
  • 資源利用率低

無限制建立執行緒

這時,我們可能會想到的另一個方法,就是為每個任務都單獨分配一個執行緒來執行,從而提高程式的響應性。因為這樣可以做到任務並行處理,能同時處理多個請求,但是要保證任務處理程式碼是執行緒安全的。

然而,這種方法也存在著一定的缺陷,極端情況下,它會建立大量的執行緒,超過了一定的數量,它繼續建立執行緒會降低程式的執行速度,甚至導致程式崩潰。所以,當需要建立大量執行緒時,它的缺點有:

  • 執行緒生命週期的開銷非常高,會消耗大量的計算資源。
  • 資源消耗問題,大量空閒執行緒會佔用很多記憶體。
  • 穩定性問題,超過了系統的限制數量,可能會導致OOM。

執行緒池的優勢

上面提到的兩種方式,都有著各自明顯的缺點。序列執行的問題在於糟糕的響應性和吞吐率;為每個任務都建立執行緒的問題在於資源管理的複雜性。這兩種方法看起來都是極端的情況,要麼只建立一個,要麼無線建立。

那麼,我們是否可以通過限制執行緒的數量,找到一種介於兩者之間平衡的方法來解決這些問題呢?答案是肯定的。各種執行策略都是一種資源管理工具,最佳的執行策略取決於可用的計算資源以及對服務質量的需求。我們可以通過限制併發任務的數量,來避免上面的兩個問題。在此,我們引入執行緒池的概念。

執行緒池,構建一定數量的工作執行緒,它與工作佇列密切相關。工作佇列儲存了所有待執行的任務,工作執行緒的主要工作就是從佇列中獲取任務,執行完成後返回執行緒池,等待下一個任務。它的優勢也非常明顯:

  • 可以重用現有執行緒,減少建立和銷燬的頻繁開銷,提高響應性。
  • 執行緒數量可調節,靈活方便,防止過多執行緒耗盡記憶體。

總之,大多數情況下,應該運用執行緒池而不是其他方式來處理併發任務。

Executor 框架的元件

說了這麼多,那到底 Executor 的框架是怎麼樣的呢?下面我們直接先來看它的框架類圖。

框架類圖

這裡寫圖片描述

下面我們根據這個框架圖,來簡單說說各個類的情況。

(1) Executor 是一個簡單的介面,如下所示:

public interface Executor {
    void execute(Runnable command);
}

(2) ExecutorService 在原有介面的基礎上,新增生命週期的方法,它包括三種狀態:執行,關閉,終止。

public interface ExecutorService extends Executor {

    void shutdown();

    List<Runnable> shutdownNow();

    boolean isShutdown();

    boolean isTerminated();

    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;

    <T> Future<T> submit(Callable<T> task);

    <T> Future<T> submit(Runnable task, T result);

    Future<?> submit(Runnable task);

    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;

    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

(3) ThreadPoolExecutor 框架核心類,Executors 工具類最終是條用它來建立不同執行策略的Executor.所以我們也可以利用它來定製機子的執行策略。

(4) Executors 工具類,可以用它來建立各種各樣的 Executor.比如下面這幾種:

- newFixedThreadPool, 固定長度的線城池。
- newCacheThreadPool, 可快取的執行緒池,可以回收空閒執行緒,任務多時則建立執行緒,數量沒有限制。
- newSingleThreadExecutor, 單執行緒。
- newScheduledThreadPool, 固定長度執行緒池,可以延遲或定時執行。

(5) ExecutorCompletionService, 它融合了Executor 和 BlockingQueue。它將計算委託給 Executor,當任務執行結束時,結果會放入佇列裡面。其實就是通過阻塞佇列作為中介來獲取結果,而不是直接使用 Future 來獲取。

任務執行的三個部分

我們先來明確任務執行的流程。一般情況下,任務的執行包含包括三個部分。

  1. 每個任務都是用 Runnable/Callable 介面的實現類表示。
  2. 任務執行的核心是採用 Executor 框架,核心類是 ThreadPoolExecutor。
  3. 非同步任務需要返回結果,提交任務後需要返回 Future, FutureTask 實現。

上面就是我們利用 Exector 框架來執行任務的一般步驟。

框架使用示意圖

它的框架使用示意圖如下:
這裡寫圖片描述

從圖中可以看出整個執行的流程。但是圖上得來終覺淺,絕知此事要躬行,還是用具體例項來演示吧。

本文完畢,具體例項的演示,請等待下一篇文章。

貌似上傳的思維導圖不夠清晰,如果想要這篇文章更清晰的思維導圖,請在我公眾號回覆[Executor]獲取。

本文原創首發於微信公眾號 [ 林裡少年 ],歡迎關注第一時間獲取更新。