思維導圖:
引言:
本章主要介紹如何選擇,配置和擴展線程池。所以,本文的所有部分都歸屬於使用部分。
- 使用部分:介紹如何選擇合適的線程池,配置合適的線程池參數,並擴展線程池的功能
一.選擇線程池
在前文中提到過,Executors可以產生不同類型的線程池,每種線程池的特性和功能不同。當我們需要執行一個或者多個任務的時候,需要選擇恰當的線程池提交併執行任務,如果選擇了不恰當的線程池可能會帶來不好的結果。
在上篇文章中也提到過,不同的任務選擇的執行策略是不同的。某些任務則和執行策略有隱性的耦合條件,致使我們只能選擇特定的線程池。以下幾個小節則分別介紹了其中一些種類的有耦合的任務。
1.1 依賴性任務
某些任務的執行需要依賴其他任務執行的結果,而如果這些返回結果的任務又被阻塞導致不能執行的話,這種情況稱之爲線程飢餓死鎖。
如果我們選擇了newSingleThreadExecutor來創建線程池又想其中提交具有依賴性的任務的話,就很有可能導致線程飢餓死鎖。如下例:RenderPageTask的執行完成需要兩個LoadFileTask的執行結果,但是由於我們選擇了單線程Executor,導致只有RenderPageTask執行完畢後纔會執行LoadFileTask任務,所以,結果就是發生了線程飢餓死鎖。
public class ThreadDeadlock {
ExecutorService exec = Executors.newSingleThreadExecutor();
public class LoadFileTask implements Callable<String> {
private final String fileName;
public LoadFileTask(String fileName) {
this.fileName = fileName;
}
public String call() throws Exception {
// Here's where we would actually read the file
return "";
}
}
public class RenderPageTask implements Callable<String> {
public String call() throws Exception {
Future<String> header, footer;
header = exec.submit(new LoadFileTask("header.html"));
footer = exec.submit(new LoadFileTask("footer.html"));
String page = renderBody();
// Will deadlock -- task waiting for result of subtask
return header.get() + page + footer.get();
}
private String renderBody() {
// Here's where we would actually render the page
return "";
}
}
}
1.2 時間敏感性任務
對於某些需要較長時間才能執行完畢的任務,則需要選擇沒有線程數量上限的線程池。否則,當執行任務的線程數量少於需要大量時間才能執行完畢的任務的數量時,很有可能會發生所有線程都執行長時間任務的情況,這就導致線程池會在相當長的一段時間內沒有響應。這時就因該選擇使用newCachedThreadPool而不是newFixedThreadPool。
1.3 線程封閉性任務
當需要執行使用了線程封閉的任務時,最好選擇使用newSingleThreadExecutor線程池,而不是多線程線程池,以防止對象泄露並確保任務不會併發的執行以喪失線程安全性。
1.4 使用ThreadLocal的任務
如果任務使用了ThreadLocal,則不要講ThreadLocal對象用於線程間的參數傳遞。
二.配置線程池
線程池有一些通用的配置參數,比如線程池的基本大小corePollSize,最大線程數量maximumPoolSize,存活時間keepAliveTime,工作隊列workQueue,線程工廠,和飽和策略處理器。本節將介紹如何配置合適的線程池參數。
2.1 設置線程池的大小
當線程池是計算密集型即處理大量運算的時候,CPU數量+1是個不錯的選擇,而當線程池是I/O密集型的時候,則需要根據任務的等待時間,執行時間等參數進行設置,總之是一句廢話,具體情況,具體分析。
2.2 線程的創建與銷燬
線程的創建與銷燬有線程池的基本大小,最大大小和存活時間這三個參數來決定。基本大小就是在沒有任務執行時所維護的線程的數量,最大大小則表示可以同時活動的線程數量的上限。如果線程空閒時間超過了存活時間,那麼線程的資源就會被回收。在我們常用的線程池中,newFixedThreadPool的基本大小和最大大小都是指定的值,而且創建的線程不會超時。newCashedThreadPool的基本大小是0,最大大小是Integer.MAX_VALUE,超時時間爲1分鐘。
2.3 管理隊列任務
在有限的線程池中會限制可併發執行的任務的數量,那麼爲執行的任務將被保存在一個隊列中。任務的基本排隊方式有三種
- 無界隊列 : 無限的提交任務,可能會導致內存溢出。
- 有界隊列 : 當待執行任務的數量達到上線後,將執行飽和策略。
- 同步移交: 通過使用SynchromousQueue直接將任務從生產者交給工作者線程以避免任務排隊。
newFixedThreadPool和newSingleThreadPool默認情況下使用的時一個無界的LinkedBlockingQueue。
2.4 飽和策略
如果我們將newFixedThreadPool和newSingleThreadPool的工作隊列修改爲有界隊列,例如ArrayBlockingQueue,有界的LinkedBlockingQueue或者PriorityBlockingQueue的話,可以更好的管理線程池所使用的資源,但是這將會導致一個問題:如果待執行任務數量達到了上限怎麼辦,此時,線程池將執行飽和策略用於處理此類問題。一般的我們有以下幾種飽和策略:
- 終止Abort : 默認的飽和策略,將會拋出RejectedExecutionExeption,我們可以捕獲並進行相應的處理。
- 拋棄Discard : 悄悄的拋棄該任務。
- 拋棄最舊的Discard-Oldest :拋棄下一個將被執行的任務,當任務隊列是PriorityBlockingQueue的時候將拋棄優先級最高的任務。
- 調用者運行Caller-Runs : 將任務回退給調用者,即在調用了executor的線程中執行該任務。
2.5 線程工廠
每單線程池需要創建線程的時候,都會通過默認的線程工廠進行創建,此時創建的線程都是新的,非守護的線程。我們可以定製自己的線程工廠以讓線程池創建獨特的線程。如下:
- 線程工廠類
public class MyThreadFactory implements ThreadFactory {
private final String poolName;
public MyThreadFactory(String poolName) {
this.poolName = poolName;
}
public Thread newThread(Runnable runnable) {
return new MyAppThread(runnable, poolName);
}
}
- 自定義線程類
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 runnable, String name) {
super(runnable, name + "-" + created.incrementAndGet());
setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
public void uncaughtException(Thread t,
Throwable e) {
log.log(Level.SEVERE,
"UNCAUGHT in thread " + t.getName(), e);
}
});
}
@Override
public void run() {
// Copy debug flag to ensure consistent value throughout.
boolean debug = debugLifecycle;
if (debug) {
log.log(Level.FINE, "Created " + getName());
}
try {
alive.incrementAndGet();
super.run();
} finally {
alive.decrementAndGet();
if (debug) {
log.log(Level.FINE, "Exiting " + getName());
}
}
}
public static int getThreadsCreated() {
return created.get();
}
public static int getThreadsAlive() {
return alive.get();
}
public static boolean getDebug() {
return debugLifecycle;
}
public static void setDebug(boolean b) {
debugLifecycle = b;
}
}
三.擴展線程池
我們可以對線程池做少量的擴展工作,例如添加前後處理或者進行使用完畢後的類似資源回收的終止處理。
我們可以重寫以下方法:
- beforeExecute:每個線程執行前調用此方法
- aferExecute : 每個線程執行後調用此方法
- terminated : 線程池完成關閉操作時調用此方法
在這個例子中,我們擴展了線程池以實現日誌和計時功能,並在線程池關閉後進行打印。
public class TimingThreadPool extends ThreadPoolExecutor {
public TimingThreadPool() {
super(1, 1, 0L, TimeUnit.SECONDS, null);
}
private final ThreadLocal<Long> startTime = new ThreadLocal<Long>();
private final Logger log = Logger.getLogger("TimingThreadPool");
private final AtomicLong numTasks = new AtomicLong();
private final AtomicLong totalTime = new AtomicLong();
@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: avg time=%dns",
totalTime.get() / numTasks.get()));
} finally {
super.terminated();
}
}
}