深入淺出執行緒池+高階選項的使用

xuxh120發表於2022-02-23

執行緒池的使用

本章將介紹對執行緒池進行配置與調優的一些高階選項, 並分析在使用任務執行框架時需要注意的各種危險, 以及一些使用 Executor的高階示例。

 

一、任務與執行策略之間的隱形耦合

1.1 隱形耦合關係

我們已經知道,Executor框架可以將任務的提交與任務的執行策略解耦開來。這為制定和修改執行策略都提供了相當大的靈活性,但並非所有的任務都能適用所有的執行策略。有些型別的任務需要明確地指定執行策略,包括:

 

a.依賴性任務。

大多數行為正確的任務都是獨立的: 它們不依賴於其他任務的執行時序、 執行結果或其他效果。 當線上程池中執行獨立的任務時, 可以隨意地改變執行緒池的大小和配置, 這些修改只會對執行效能產生影響。 然而,如果提交給執行緒池的任務需要依賴其他的任務, 那麼就隱含地給執行策略帶來了約束, 此時必須小心地維持這些執行策略以避免產生活躍性問題。

 

b.使用執行緒封閉機制的任務。

與執行緒池相比, 單執行緒的Executor能夠對併發性做出更強的承諾。 它們能確保任務不會併發地執行, 使你能夠放寬程式碼對執行緒安全的要求。 物件可以封閉在 任務執行緒中,使得在該執行緒中執行的任務在訪問該物件時不需要同步, 即使這些資源不是執行緒 安全的也沒有問題。 這種情形將在任務與執行策略之間形成隱式的耦合---任務要求其執行所在的Executor是單執行緒的e。如果將Executor從單執行緒環境改為執行緒池環境, 那麼將會失去執行緒安全性。

 

c.對響應時間敏感的任務。

如果將一個執行時間較長的任務提交到單執行緒的Executor中,或者將多個執行時間較長的任務提交到一個只包含少量執行緒的執行緒池中,那麼將降低由該Executor管理的服務的響應性。

 

d.使用ThreadLocal的任務

ThreadLocal使每個執行緒都可以擁有某個變數的一個私有“版本“。然而,只要條件允許,Executor可以自由地重用這些執行緒。在標準的Executor實現中,當執行需求較低時將回收空閒執行緒,而當需求增加時將新增新的執行緒,並且如果從任務中丟擲了一個未檢查異常,那麼將用一個新的工作者執行緒來替代丟擲異常的執行緒。只有當執行緒本地值 的生命週期受限於任務的生命週期時,線上程池的執行緒中使用ThreadLocal才有意義,而線上 程池的執行緒中不應該使用 ThreadLocal在任務之間傳遞值。

 

只有當任務都是同型別的並且相互獨立時,執行緒池的效能才能達到最佳。如果將執行時間較長的與執行時間較短的任務混合在一起,那麼除非執行緒池很大,否則將可能造成 “擁塞 ”。如果提交的任務依賴於其他任務,那麼除非執行緒池無限大,否則將可能造成死鎖。

 

1.2 執行緒飢餓死鎖

線上程池中,如果任務依賴於其他任務,那麼可能產生死鎖。在單執行緒的Executor中,如 果一個任務將另一個任務提交到同一個Executor,並且等待這個被提交任務的結果,那麼通常會引發死鎖。第二個任務停留在工作佇列中,並等待第一個任務完成,而第一個任務又無法完 成,因為它在等待第二個任務的完成。

在更大的執行緒池中, 如果所有正在執行任務的執行緒都由於等待其他仍處在工作佇列中的任務而阻塞,那麼會發生同樣的問題。這種現象被稱為飢餓死鎖(Thread Starvation Deadlock)。

 

1.3 執行時間較長的任務

如果任務阻塞的時間過長, 那麼即使不出現死鎖, 執行緒池的響應性也會變得糟糕。執行時 間較長的任務不僅會造成執行緒池堵塞, 甚至還會增加執行時間較短任務的服務時間。如果執行緒 池中執行緒的數量遠小於在穩定狀態下執行時間較長任務的數量, 那麼到最後可能所有的執行緒都會執行這些執行時間較長的任務, 從而影響整體的響應性。

 

有一項技術可以緩解執行時間較長任務造成的影響,限定任務等待資源的時間, 而不要無限制地等待。在平臺類庫的大多數可阻塞方法中, 都同時定義了限時版本和無限時版本, 例如Thread.join、BlockingQueue.put、CountDownLatch.await以及Selector.select等。如果等待超時,那麼可以把任務標識為失敗,然後中止任務或者將任務重新放回佇列以便隨後執行。這樣, 無論任務的最終結果是否成功, 這種辦法都能確保任務總能繼續執行下去, 並將執行緒釋放 出來以執行一些能更快完成的任務。如果線上程池中總是充滿了被阻塞的任務, 那麼也可能表明執行緒池的規模過小。

 

 

二、設定執行緒池的大小

要設定執行緒池的大小也並不困難, 只需要避免 “過大” 和 “過小” 這兩種極端情況。如果執行緒池過大,那麼大量的執行緒將在相對很少的CPU和記憶體資源上發生競爭, 這不僅會導致更高的記憶體使用量, 而且還可能耗盡資源。如果執行緒池過小, 那麼將導致許多空閒的處理器無法執行工作,從而降低吞吐率。

 

 

 要想正確地設定執行緒池的大小,必須分析計算環境、資源預算和任務的特性。在部署的系統中有多少個CPU? 多大的記憶體?任務是計算密集型、I/0密集型還是二者皆可?等等。

 

  • 對於計算密集型的任務,在擁有N個處理器的系統上,當執行緒池的大小為N+1時,通常能實現最優的利用率。(即使當計算密集型的執行緒偶爾由於頁缺失故障或者其他原因而暫停時,這個 “額外 ” 的執行緒也能確保CPU的時鐘週期不會被浪費。)

 

  • 對於I/O密集型或者其他阻塞操作的任務,由於執行緒並不會一直執行,因此執行緒池的規模應該更大。要正確地設定執行緒 池的大小,一種方法是通過另來調節執行緒池的大小:在某個基準負載下,分別設定不同大小的執行緒池來執行應用程式,並觀察CPU利用率的水平。還可以估算出任務的等待時間與計算時間的比值,通過計算獲得合適的執行緒池大小:

Ncpu   =  number of CPUs

Ucpu   =  target CPU utilization,  0 ≤ Ucpu  ≤ 1

W / C  = ratio of wait time to compute time

要使處理器達到期望的使用率,執行緒池的最優大小等於

Nthread  =  Ncpu  *  Ucpu  *  (1   +   W / C

可以通過 Runtime獲得CPU的數目:

Int N_CPUS  =  Runtime.getRuntime().availableProcessors();

 

當然,CPU週期並不是唯一影響執行緒池大小的資源,還包括記憶體、檔案控制程式碼、套接字控制程式碼和資料庫連線等。計算這些資源對執行緒池的約束條件是更容易的:計算每個任務對該資源的需求量,然後用該資源的可用總量除以每個任務的需求量,所得結果就是執行緒池大小的上限。

 

當任務需要某種通過資源池來管理的資源時,例如資料庫連線,那麼執行緒池和資源池的大小將會相互影響。如果每個任務都需要一個資料庫連線,那麼連線池的大小就限制了執行緒池的 大小。同樣,當執行緒池中的任務是資料庫連線的唯一使用者時,那麼執行緒池的大小又將限制連 接池的大小。

 

 

 

 

 

 

 

 

三、配置 ThreadPoolExecutor

ThreadPoolExecutor為一些Executor提供了基本的實現,這些Executor是由 Executors 的newCachedThreadPool、newFixedThreadPool和newScheduledThreadExecutor等工廠方法返回的。 ThreadPoolExecutor是一個靈活的、穩定的執行緒池,允許進行各種定製。

 

如果預設的執行策略不能滿足需求,那麼可以通過 ThreadPoolExecutor的建構函式來例項化一個物件,並根據自己的需求來定製,並且可以參考Executors的原始碼來了解預設配置下的執行策略, 然後再以這些執行策略為基礎進行修改。ThreadPoolExecutor定義了很多構造數, 在程式清單8-2中給出了最常見的形式。

程式碼 8-2  ThreadPoolExecutor的通用建構函式

public ThreadPoolExecutor(int corePoolSize,

                              int maximumPoolSize,

                              long keepAliveTime,

                              TimeUnit unit,

                              BlockingQueue<Runnable> workQueue,

                              ThreadFactory threadFactory,

                              RejectedExecutionHandler handler) { … }

 

3.1 執行緒的建立與銷燬

執行緒池的基本大小(Core Pool Size)、最大大小(Maximum Pool Size)以及存活時間等因素共同負責執行緒的建立與銷燬。 

  • 基本大小也就是執行緒池的目標大小,即在沒有任務執行時執行緒池的大小,並且只有在工作佇列滿了的情況下才會建立超出這個數量的執行緒。 
  • 執行緒池的最大大小表示可同時活動的執行緒數最的上限。如果某個執行緒的空閒時間超過了存活時間,那麼將被標記為可回收的,並且當執行緒池的當前大小超過了基本大小時,這個執行緒將被終止。

通過調節執行緒池的基本大小和存活時間,可以幫助執行緒池回收空閒執行緒佔有的資源,從而使得這些資源可以用於執行其他工作。顯然,這是種折衷: 回收空閒執行緒會產生額外的延遲,因為當需求增加時,必須建立新的執行緒來滿足需求。

 

  • newFixedThreadPool工廠方法將執行緒池的基本大小和最大大小設定為引數中指定的值,而且建立的執行緒池不會超時。
  • newCachedThreadPool工廠方法將執行緒池的最大大小設定為Integer.MAX_VALUE, 而將基本大小設定為零,並將超時設定為1分鐘,這種方法建立出來的執行緒池可以被無限擴充套件,並且當需求降低時會自動收縮。

其他形式的執行緒池可以通過顯式的 ThreadPoolExecutor建構函式來構造。

 

3.2 管理佇列任務

在有限的執行緒池中會限制可併發執行的任務數量。(單執行緒的Executor是一種值得注意的特例:它們能確保不會有任務併發執行,因為它們通過執行緒封閉來實現執行緒安全性。)

 

如果無限制地建立執行緒,那麼將導致不穩定性,並通過採用固定大小的執行緒池來解決這個問題,而不是每收到一個請求就建立一個新執行緒 。然而,這個方案並不完整。在高負載情況下,應用程式仍可能耗盡資源,只是出現問題的概率較小。

  • 如果新請求的到達速率超過了執行緒池的處理速率,那麼新到來的請求將累積起來。線上程池中,這些請求會在一個由 Executor管理的 Runnable佇列中等待,而不會像執行緒那樣去競爭CPU資源。通過一個Runnable和一個連結串列節點來表現一個等待中的任務,當然比使用執行緒來表示的開銷低很多,但如果客戶提交給伺服器請求的速率超過了伺服器的處理速率,那麼仍可能會耗盡資源。
  • 即使請求的平均到達速率很穩定,也仍然會出現請求突增的情況。儘管佇列有助於緩解任務的突增問題,但如果任務持續高速地到來,那麼最終還是會抑制請求的到達率以避免耗盡記憶體。甚至在耗盡記憶體之前,響應效能也將隨著任務佇列的增長而變得越來越糟。

 

ThreadPoolExecutor 允許提供一個 BlockingQueue 來儲存等待執行的任務。 基本的任務排隊方法有 3 種:無界佇列、有界佇列和同步移交 (Synchronous Handoff)。佇列的選擇與其他的配置引數有關,例如執行緒池的大小等。

 

newFixedThreadPool 和 newSingleThreadExecutor在預設情況下將使用一個無界的 LinkedBlockingQueue

  • 如果所有工作者執行緒都處於忙碌狀態,那麼任務將在佇列中等候。
  • 如果任務持續快速地到達,並且超過了執行緒池處理它們的速度, 那麼佇列將無限制地增加。

 

一種更穩妥的資源管理策略是使用有界佇列,例如 ArrayBlockingQueue、有界的LinkedBlockingQueue、 PriorityBlockingQueue。在使用有界的工作佇列時,佇列的大小與執行緒池的大小必須一起調節。如果執行緒池較小而佇列較大,那麼有助於減少記憶體使用量,降低 CPU的使用率,同時還可以減少上下文切換,但付出的代價是可能會限制吞吐量。

有界佇列有助於避免資源耗盡的情況發生,但它又帶來了新的問題:當佇列填滿後,新的任務該怎麼辦?(有許多飽和策略 (Saturation Policy] 可以解決這個問題)

 

對於非常大的或者無界的執行緒池,可以通過使用 SynchronousQueue 來避免任務排隊,以及直接將任務從生產者移交給工作者執行緒。SynchronousQueue 不是一個真正的佇列,而是一 種線上程之間進行移交的機制。要將一個元素 放入 SynchronousQueue 中,必須有另一個執行緒正在等待接受這個元素。如果沒有執行緒正在等待,並且執行緒池的當前大小小於最大值,那麼ThreadPoolExecutor 將建立一個新的執行緒, 否則根據飽和策略,這個任務將被拒絕。

使用直接移交將更高效,因為任務會直接移交給執行它的執行緒,而不是被首先放在佇列中,然後由工作者執行緒從佇列中提取該任務。但是,只有當執行緒池是無界的或者可以拒絕任務時, SynchronousQueue才有實際價值。在newCachedThreadPool 工廠方法中就使用了 SynchronousQueue。

 

當使用像 LinkedBlockingQueue 或 ArrayBlockingQueue 這樣的 FIFO(先進先出)佇列時,任務的執行順序與它們的到達順序相同。如果想進一步控制任務執行順序,還可以使用PriorityBlockingQueue,這個佇列將根據優先順序來安排任務。任務的優先順序是通過自然順序或Comparator(如果任務實現了Comparable)來定義的。

 

3.3 飽和策略

當有界佇列被填滿後,飽和策略開始發揮作用。ThreadPoolExecutor的飽和策略可以通過呼叫setRejectedExecutionHandler來修改。(如果某個任務被提交到一個巳被關閉的Executor時,也會用到飽和策略。)JDK提供了幾種不同的RejectedExecutionHandler實現,每種實現都包含有不固的飽和策略:AbortPolicy、CallerRunsPolicy、DiscardPolicy和DiscardOldestPolicy

  1. "中止(Abort)"策略是預設的飽和策略,該策略將丟擲未檢查的RejectedExecution­-Exception。呼叫者可以捕獲這個異常,然後根據需求編寫自己的處理程式碼。
  2. 當新提交的任務法儲存到佇列中等待執行時,“拋棄(Discard)"策略會悄悄拋棄該任務。
  3. “拋棄最舊的( Discard-Oldest)"策略則會拋棄下一個將被執行的任務,然後嘗試重新提交新的任務。如果工 作佇列是一個優先佇列,那麼“拋棄最舊的”策略將導致拋棄優先順序最高的任務,因此最好不要將“拋棄最舊的"飽和策略和優先順序佇列放在一起使用。
  4. “呼叫者執行(Caller-Runs)"策略實現了一種調節機制,該策略既不會拋棄任務,也不會丟擲異常, 而是將某些任務回退到呼叫者,從而降低新任務的流量。它不會線上程池的某個執行緒中執行新提交的任務, 而是在一個呼叫了execute的執行緒中執行該任務。 

 

我們可以將WebServer示例修改為使用有界佇列和“呼叫者執行” 飽和策略,當執行緒池中的所有執行緒都被佔用,並且工作佇列被填滿後,下一個任務會在呼叫execute時在主執行緒中執行由於執行任務需要一定的時間,因此主執行緒至少在一段時間內不能提交任何任務,從而使得工作者執行緒有時間來處理完正在執行的任務。在這期間,主執行緒不會呼叫accept, 因此到達的請求將被儲存TCP層的佇列中而不是在應用程式的佇列中。如果持續過載,那麼TCP層將最終發現它的請求佇列被填滿,因此同樣會開始拋棄請求。當伺服器過載時,這種過載情況會逐漸向外蔓延開來-從執行緒池到工作佇列到應用程式再到TCP層,最終達到客戶端,導致伺服器在高負載下實現一種平緩的效能降低。

程式碼 8-3  建立一個固定大小的執行緒池,採用有屆佇列以及呼叫者運營飽和策略

ThreadPoolExecutor executor = new ThreadPoolExecutor(nThreads, nThreads,

                                0L, TimeUnit.MILLISECONDS,

                                new LinkedBlockingQueue<Runnable>());

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

 

當工作佇列被填滿後,沒有預定義的飽和策略來阻塞execute。然而,通過使用Semaphore(訊號量)來限制任務的到達率,就可以實現這個功能。在程式清單 8-4 的BoundedExecutor中給出了這種方法。該方法使用了一個無界佇列(因為不能限制佇列的大小和任務的到達率),並設定訊號量的上界設定為執行緒池的大小加上可排隊任務的數量,這是因為訊號量需要控制正在執行的和等待執行的任務數量。

 

程式碼 8-4 使用Semaphore來控制任務的提交速率

class BoundedExecutor {

    private final Executor exec;

    private final Semaphore semaphore;

 

    public BoundedExecutor(Executor exec, int bound) {

        this.exec = exec;

        this.semaphore = new Semaphore(bound);

    }

 

    public void submitTask(final Runnable command) {

        try {

            //提交任務前請求訊號量

            semaphore.acquire(); 

            exec.execute(new Runnable() {

                @Override

                public void run() {

                    try {

                        command.run();

                    } finally {

                        //執行完釋放訊號

                        semaphore.release(); 

                    }

                }

            });

        } catch (InterruptedException e) {

            // handle exception

        }

    }

}

 

3.4 執行緒工廠

每當執行緒池需要建立一個執行緒時,都是通過執行緒工廠方法(請參見程式清單8-5)來完成的。預設的執行緒工廠方法將建立一個新的、非守護的執行緒,並且不包含特殊的配置資訊。通過指定一個執行緒工廠方法,可以定製執行緒池的配置資訊。在ThreadFactory 中只定義了一個方法newThread, 每當執行緒池需要建立一個新執行緒時都會呼叫這個方法。

 

然而,在許多情況下都需要使用定製的執行緒工廠方法。例如, 

  • 為執行緒池中的執行緒指定一個UncaughtExceptionHandler, 
  • 例項化一個定製的Thread 類用於執行除錯資訊的記錄。
  • 修改執行緒的優先順序(這通常並不是一個好主意。請參見10.3.1節)或者守護狀態(同樣, 這也不是一個好主意。請參見7.4.2節)。
  • 給執行緒取一個更有意義的名稱,用來解釋執行緒的轉儲資訊和錯誤日誌。

 

程式碼 8-5 ThreadFactory介面

public interface ThreadFactory {

    /**

     * Constructs a new {@code Thread}.  Implementations may also initialize

     * priority, name, daemon status, {@code ThreadGroup}, etc.

     *

     * @param r a runnable to be executed by new thread instance

     * @return constructed thread, or {@code null} if the request to

     *         create a thread is rejected

     */

    Thread newThread(Runnable r);

}

 

 

在程式清單8-6 的MyThreadFactory 中給出了一個自定義的執行緒工廠。它建立了一個新的My App Thread 例項, 並將一個特定千執行緒池的名字傳遞給MyAppThread的建構函式,從而可以線上程轉儲和錯誤日誌資訊中區分來自不同執行緒池的執行緒。在應用程式的其他地方也可以使用MyAppThread, 以便所有執行緒都能使用它的除錯

 

MyAppThread中還可以定製其他行為,如程式清單8-6所示,包括:為執行緒指定名字,設定自定義UncaughtExceptionHandler 向Logger 中寫入資訊,維護一些統計資訊(包括有多少個執行緒被建立和銷燬),以及線上程被建立或者終止時把除錯訊息寫入日誌。

 

程式碼 8-6

 

public class MyThreadFactory implements ThreadFactory {

    private final String poolName;

 

    public MyThreadFactory(String poolName) {

        super();

        this.poolName = poolName;

    }

 

    @Override

    public Thread newThread(Runnable r) {

        return new MyAppThread(r);

    }

}

 

public class MyAppThread extends Thread {

    public static final String DEFAULT_NAME = "MyAppThread";

    private static volatile boolean debugLifecycle = false;

    private static final AtomicInteger created = new AtomicInteger();

    private static final AtomicInteger alive = new AtomicInteger();

    private static final Logger log = Logger.getAnonymousLogger();

 

    public MyAppThread(Runnable r) {

        this(r, DEFAULT_NAME);

    }

 

    public MyAppThread(Runnable r, String name) {

        super(r, name + "-" + created.incrementAndGet());

        setUncaughtExceptionHandler( //設定未捕獲的異常發生時的處理器

                new Thread.UncaughtExceptionHandler() {

                    @Override

                    public void uncaughtException(Thread t, Throwable e) {

                        log.log(Level.SEVERE, "UNCAUGHT in thread " + t.getName(), e);

                    }

                });

    }

 

    @Override

    public void run() {

        boolean debug = debugLifecycle;

        if (debug)

            log.log(Level.FINE, "running thread " + getName());

        try {

            alive.incrementAndGet();

            super.run();

        } finally {

            alive.decrementAndGet();

            if (debug)

                log.log(Level.FINE, "existing thread " + getName());

        }

    }

 

    public static boolean getDebug() {  return debugLifecycle;  }

 

    public static void setDebug(boolean b) { debugLifecycle = b; }

}

 

如果在應用程式中需要利用安全策略來控制對某些特殊程式碼庫的訪問許可權, 那麼可以通過 Executor 中的 privilegedThreadFactory 工廠來定製自己的執行緒工廠。 通過這種方式建立出來的 執行緒, 將與建立 privilegedThreadFactory 的執行緒擁有相同的訪問許可權、 AccessControlContext 和 contextClassLoader

如果不使用 privilegedThreadFactory, 執行緒池建立的執行緒將從在需要新 執行緒時呼叫 execute 或 submit 的客戶程式中繼承訪問許可權, 從而導致令人困惑的安全性異常。

 

3.5 在呼叫建構函式後再定製ThreadPoolExecutor

在呼叫完 ThreadPoolExecutor 的建構函式後, 仍然可以通過設定函式 (Setter) 來修改大多數傳遞給它的建構函式的引數(例如執行緒池的基本大小、 最大大小、 存活時間、 執行緒工廠以及拒絕執行處理器 (Rejected Execution Handler)) 如果Executor 是通過 Executors 的某個 (newSingleTbreadExecutor 除外)工廠方法建立的, 那麼可以將結果的型別轉換為 ThreadPoolExecutor 以訪問設定器, 如程式清單 8-7 所示。

程式碼 8-7  對標準工廠方法建立的執行緒池進行修改

ExecutorService exec = Executors.newCachedThreadPool();

if (exec instanceof ThreadPoolExecutor) {

    ((ThreadPoolExecutor) exec).setCorePoolSize(10);

} else {

    throw new AssertionError("不能轉換");

}

在 Executors中包含一個 unconfiurableExecutorService 工廠方法, 該方法對一個現有的 ExecutorService 進行包裝, 使其只暴露出 ExecutorService 的方法, 因此不能對它進行配置。 newSingleThreadExecutor 返回按這種方式封裝的 ExecutorService, 而不是最初的 ThreadPoolExecutor。雖然單執行緒的 Executor 實際上被實現為一個只包含唯一執行緒的執行緒池,但它同樣確保了不會併發地執行任務。如果在程式碼中增加單執行緒 Executor 的執行緒池大小, 那麼將破壞它的執行語義。

 

你可以在自己的 Executor 中使用這項技術以防止執行策略被修改。如果將 ExecutorService 暴露給不信任的程式碼, 又不希望對其進行修改,就可以通過 unconfigurableExecutorService 包裝它。·

 

 

 

四、擴充套件 ThreadPoolExecutor

ThreadPoolExecutor 是可擴充套件的, 它提供了幾個可以在子類化中改寫的方法: beforeExecute、 afteExecute 和 terminated, 這些方法可以用於擴充套件 ThreadPoolExecutor 的行為。

 

在執行任務的執行緒中將呼叫 beforeExecute 和 afterExecute 等方法,在這些方法中還可以新增日誌、計時、監視或統計資訊收集的功能。無論任務是從 run 中正常返回,還是丟擲一個 異常而返回, afterExecute 都會被呼叫。(如果任務在完成後帶有一個 Error, 那麼就不會呼叫 after Execute。)如果 beforeExecute 丟擲一個 RuntimeException, 那麼任務將不被執行, 並且 afterExecute 也不會被呼叫。JDK版本不同,此處邏輯也有所不同。

 

線上程池完成關閉操作時呼叫 terminated, 也就是在所有任務都已經完成並且所有工作者執行緒也巳經關閉後。 terminated 可以用來釋放 Executor 在其生命週期裡分配的各種資源, 此外還可以執行傳送通知、 記錄日誌或者收集 finalize 統計資訊等操作。

 

示例:給執行緒池新增統計資訊,如程式碼 8-8,TimingThreadPool增加了日誌記錄和任務執行時間統計,並記錄已經處理的任務數和總的時間,以及輸出任務平均執行時間的日誌訊息。

程式碼 8-8 增加了日誌和計時等功能的執行緒池

public class TimingThreadPool extends ThreadPoolExecutor {

    private final ThreadLocal<Long> startTime = new ThreadLocal<Long>();  
    private final Logger log = Logger.getAnonymousLogger();

    private final AtomicLong numTasks = new AtomicLong(); //統計任務數

    private final AtomicLong totalTime = new AtomicLong(); //執行緒池執行總時間

 

    public TimingThreadPoolExecutor(int corePoolSize, int maximumPoolSize,

                                    long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {

        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);

    }

 

    @Override

    protected void beforeExecute(Thread t, Runnable r) {

        super.beforeExecute(t, r);

        log.fine(String.format("Thread %s: start %s", t, r));

        startTime.set(System.nanoTime());

    }

 

    @Override

    protected void afterExecute(Runnable r, Throwable t) {

        try {

            long endTime = System.nanoTime();

            long taskTime = endTime - startTime.get();

            numTasks.incrementAndGet();

            totalTime.addAndGet(taskTime);

            log.fine(String.format("Thread %s: end %s, time=%dns", t, r, taskTime));

        } finally {

            super.afterExecute(r, t);

        }

    }

 

    @Override

    protected void terminated() {

        try {

            //任務執行平均時間

            log.info(String.format("Terminated: average time=%dns", totalTime.get() / numTasks.get()));

        } finally {

            super.terminated();

        }

    }

}

相關文章