文章目錄
一、爲什麼要自定義線程池
阿里規範中對於線程、線程池的規定
《阿里巴巴 Java開發手冊》1.6併發處理
第3條規定:線程資源必須通過線程池提供,不允許在應用中自行顯式創建線程
第4條規定:線程池不允許使用Executors創建,而是通過ThreadPoolExecutor的方式創建,這樣的處理方式能讓編寫代碼的攻城獅更加明確線程池的運行規則,規避資源耗盡(OOM)的風險
之所以會出現這樣的規範,是因爲jdk已經封裝好的線程池存在潛在風險:
-
FixedThreadPool 和 SingleThreadPool:
允許的請求隊列長度爲 Integer.MAX_VALUE ,會堆積大量請求OOM -
CachedThreadPool 和 ScheduledThreadPool:
允許的創建線程數量爲 Integer.MAX_VALUE,可能會創建大量線程OOM
所以從系統安全角度出發,原則上都應該自己手動創建線程池
二、如何自定義線程池
ThreadPoolExecutor 有多個重載的構造函數。這裏使用參數最多的一個簡要說明自定義線程池的關鍵參數。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
其實自定義線程池很簡便,就這麼幾個規則
- 線程池的線程數量長期維持在 corePoolSize 個(核心線程數量)
- 線程池的線程數量最大可以擴展到 maximumPoolSize 個
- 在 corePoolSize ~ maximumPoolSize 這個區間的線程,一旦空閒超過keepAliveTime時間,就會被殺掉(時間單位)
- 送來工作的線程數量超過最大數以後,送到 workQueue 裏面待業
- 待業隊伍也滿了,就按照事先約定的策略 RejectedExecutionHandler 給拒絕掉
以下詳細解析拒絕策略
三、線程池的拒絕策略
3-0、所有拒絕策略都實現了接口 RejectedExecutionHandler
public interface RejectedExecutionHandler {
/**
* @param r the runnable task requested to be executed
* @param executor the executor attempting to execute this task
* @throws RejectedExecutionException if there is no remedy
*/
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
這個接口只有一個 rejectedExecution 方法。
r 爲待執行任務;executor 爲線程池;方法可能會拋出拒絕異常。
3-1、AbortPolicy
直接拋出拒絕異常(繼承自RuntimeException),會中斷調用者的處理過程,所以除非有明確需求,一般不推薦
public static class AbortPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
3-2、CallerRunsPolicy
在調用者線程中(也就是說誰把 r 這個任務甩來的),運行當前被丟棄的任務。
只會用調用者所在線程來運行任務,也就是說任務不會進入線程池。
如果線程池已經被關閉,則直接丟棄該任務。
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
這裏有個小問題:r.run() 是如何做到使用調用者所在線程來運行任務的?
3-3、DiscardOledestPolicy
丟棄隊列中最老的,然後再次嘗試提交新任務。
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
這裏 e.getQueue() 是獲得待執行的任務隊列,也就是前面提到的待業隊列。
因爲是隊列,所以先進先出,一個poll()方法就能直接把隊列中最老的拋棄掉,再次嘗試執行execute®。
這個隊列在線程池定義的時候就能看到,是一個阻塞隊列
/**
* The queue used for holding tasks and handing off to worker
* threads. We do not require that workQueue.
*/
private final BlockingQueue<Runnable> workQueue;
public BlockingQueue<Runnable> getQueue() {
return workQueue;
}
3-4、DiscardPolicy
默默丟棄無法加載的任務。
這個代碼就很簡單了,真的是啥也沒做
public static class DiscardPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
3-5、通過實現 RejectedExecutionHandler 接口擴展
jdk內置的四種拒絕策略(都在ThreadPoolExecutor.java裏面)代碼都很簡潔易懂。
我們只要繼承接口都可以根據自己需要自定義拒絕策略。下面看兩個例子。
一是netty自己實現的線程池裏面私有的一個拒絕策略。單獨啓動一個新的臨時線程來執行任務。
private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
final Thread t = new Thread(r, "Temporary task executor");
t.start();
} catch (Throwable e) {
throw new RejectedExecutionException(
"Failed to start a new thread", e);
}
}
}
另外一個是dubbo的一個例子,它直接繼承的 AbortPolicy ,加強了日誌輸出,並且輸出dump文件
public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
String msg = String.format("Thread pool is EXHAUSTED!" +
" Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)," +
" Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!",
threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(), e.getLargestPoolSize(),
e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(),
url.getProtocol(), url.getIp(), url.getPort());
logger.warn(msg);
dumpJStack();
throw new RejectedExecutionException(msg);
}
}