深入淺出執行緒池+高級選項的使用
執行緒池的使用
本章將介紹對執行緒池進行配置與調優的一些高級選項, 並分析在使用任務執行框架時需要注意的各種危險, 以及一些使用 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。
- “中止(Abort)”策略是默認的飽和策略,該策略將拋出未檢查的RejectedExecution-Exception。調用者可以捕獲這個異常,然後根據需求編寫自己的處理程式碼。
- 當新提交的任務法保存到隊列中等待執行時,「拋棄(Discard)”策略會悄悄拋棄該任務。
- 「拋棄最舊的( Discard-Oldest)”策略則會拋棄下一個將被執行的任務,然後嘗試重新提交新的任務。如果工 作隊列是一個優先隊列,那麼「拋棄最舊的」策略將導致拋棄優先順序最高的任務,因此最好不要將「拋棄最舊的"飽和策略和優先順序隊列放在一起使用。
- 「調用者運行(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 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(); } } } |

