詳解 ThreadPoolExecutor
線程池是爲了避免線程頻繁的創建和銷燬帶來的性能消耗,而建立的一種池化技術,它是把已創建的線程放入“池”中,當有任務來臨時就可以重用已有的線程,無需等待創建的過程,這樣就可以有效提高程序的響應速度。但如果要說線程池的話一定離不開 ThreadPoolExecutor ,在阿里巴巴的《Java 開發手冊》中是這樣規定線程池的:
線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的讀者更加明確線程池的運行規則,規避資源耗盡的風險。
說明:Executors 返回的線程池對象的弊端如下:
- FixedThreadPool 和 SingleThreadPool:允許的請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM【內存耗盡】;
- CachedThreadPool 和 ScheduledThreadPool:允許的創建線程數量爲 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM。
當我們去看 Executors 的源碼會發現,Executors.newFixedThreadPool()、Executors.newSingleThreadExecutor() 和 Executors.newCachedThreadPool() 等方法的底層都是通過 ThreadPoolExecutor 實現的,它有哪些核心的參數?它是如何工作的?
1)ThreadPoolExecutor 的參數含義
ThreadPoolExecutor 的核心參數指的是它在構建時需要傳遞的參數,其構造方法如下所示:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
// maximumPoolSize 必須大於 0,且必須大於 corePoolSize
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- 參數1 :corePoolSize 表示線程池的常駐核心線程數;
如果設置爲 0,則表示在沒有任何任務時,銷燬線程池;如果大於 0,即使沒有任務時也會保證線程池的線程數量等於此值。但需要注意,此值如果設置的比較小,則會頻繁的創建和銷燬線程(創建和銷燬的原因會在本課時的下半部分講到);如果設置的比較大,則會浪費系統資源,所以開發者需要根據自己的實際業務來調整此值。 - 參數 2:maximumPoolSize 表示線程池在任務最多時,最大可以創建的線程數;
官方規定此值必須大於 0,也必須大於等於 corePoolSize,此值只有在任務比較多,且不能存放在任務隊列時,纔會用到。 - 參數3:keepAliveTime 表示線程的存活時間;
當線程池空閒時並且超過了此時間,多餘的線程就會銷燬,直到線程池中的線程數量銷燬的等於 corePoolSize 爲止,如果 maximumPoolSize 等於 corePoolSize,那麼線程池在空閒的時候也不會銷燬任何線程。 - 參數4:unit 表示存活時間的單位;
它是配合 keepAliveTime 參數共同使用的。 - 參數5:workQueue 表示線程池執行的任務隊列;
當線程池的所有線程都在處理任務時,如果來了新任務就會緩存到此任務隊列中排隊等待執行。 - 參數6:threadFactory 表示線程的創建工廠;
此參數一般用的比較少,我們通常在創建線程池時不指定此參數,它會使用默認的線程創建工廠的方法來創建線程,源代碼如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
// Executors.defaultThreadFactory() 爲默認的線程創建工廠
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
public static ThreadFactory defaultThreadFactory() {
return new DefaultThreadFactory();
}
// 默認的線程創建工廠,需要實現 ThreadFactory 接口
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
// 創建線程
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false); // 創建一個非守護線程
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY); // 線程優先級設置爲默認值
return t;
}
}
我們也可以自定義一個線程工廠,通過實現 ThreadFactory 接口來完成,這樣就可以自定義線程的名稱或線程執行的優先級了。
- 參數7:RejectedExecutionHandler 表示指定線程池的拒絕策略;
當線程池的任務已經在緩存隊列 workQueue 中存儲滿了之後,並且不能創建新的線程來執行此任務時,就會用到此拒絕策略,它屬於一種限流保護的機制。
線程池的工作流程要從它的執行方法 execute() 說起,源碼如下:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 當前工作的線程數小於核心線程數
if (workerCountOf(c) < corePoolSize) {
// 創建新的線程執行此任務
if (addWorker(command, true))
return;
c = ctl.get();
}
// 檢查線程池是否處於運行狀態,如果是則把任務添加到隊列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再出檢查線程池是否處於運行狀態,防止在第一次校驗通過後線程池關閉
// 如果是非運行狀態,則將剛加入隊列的任務移除
if (! isRunning(recheck) && remove(command))
reject(command);
// 如果線程池的線程數爲 0 時(當 corePoolSize 設置爲 0 時會發生)
else if (workerCountOf(recheck) == 0)
addWorker(null, false); // 新建線程執行任務
}
// 核心線程都在忙且隊列都已爆滿,嘗試新啓動一個線程執行失敗
else if (!addWorker(command, false))
// 執行拒絕策略
reject(command);
}
其中 addWorker(Runnable firstTask, boolean core) 方法的參數說明如下:
firstTask:線程應首先運行的任務,如果沒有則可以設置爲 null;
core:判斷是否可以創建線程的閥值(最大值),如果等於 true 則表示使用 corePoolSize 作爲閥值,false 則表示使用 maximumPoolSize 作爲閥值。
2)線程池任務執行的主要流程
線程池任務執行的主要流程,可以參考以下流程圖:
3)知識擴展
3.1)execute() 和 submit() 的區別
execute() 和 submit() 都是用來執行線程池任務的,它們最主要的區別是,submit() 方法可以接收線程池執行的返回值,而 execute() 不能接收返回值。來看兩個方法的具體使用:
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 10, 10L,
TimeUnit.SECONDS, new LinkedBlockingQueue(20));
// execute 使用
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("Hello, execute.");
}
});
// submit 使用
Future<String> future = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("Hello, submit.");
return "Success";
}
});
System.out.println(future.get());
以上程序執行結果如下:
Hello, submit.
Hello, execute.
Success
從以上結果可以看出 submit() 方法可以配合 Futrue 來接收線程執行的返回值。它們的另一個區別是 execute() 方法屬於 Executor 接口的方法,而 submit() 方法則是屬於 ExecutorService 接口的方法,它們的繼承關係如下圖所示:
3.2)線程池的拒絕策略
當線程池中的任務隊列已經被存滿,再有任務添加時會先判斷當前線程池中的線程數是否大於等於線程池的最大值,如果是,則會觸發線程池的拒絕策略。Java 自帶的拒絕策略有 4 種:
- AbortPolicy:終止策略,線程池會拋出異常並終止執行,它是默認的拒絕策略;
- CallerRunsPolicy,把任務交給當前線程來執行;
- DiscardPolicy,忽略此任務(最新的任務);
- DiscardOldestPolicy,忽略最早的任務(最先加入隊列的任務)。
演示一個 AbortPolicy 的拒絕策略,代碼如下:
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 3, 10,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(2),
new ThreadPoolExecutor.AbortPolicy()); // 添加 AbortPolicy 拒絕策略
for (int i = 0; i < 6; i++) {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName());
});
}
以上程序的執行結果:
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-3
pool-1-thread-2
Exception in thread “main” java.util.concurrent.RejectedExecutionException: Task com.lagou.interview.ThreadPoolExample$$LambdaAbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at com.lagou.interview.ThreadPoolExample.rejected(ThreadPoolExample.java:35)
at com.lagou.interview.ThreadPoolExample.main(ThreadPoolExample.java:26)
可以看出當第 6 個任務來的時候,線程池則執行了 AbortPolicy 拒絕策略,拋出了異常。因爲隊列最多存儲 2 個任務,最大可以創建 3 個線程來執行任務(2+3=5),所以當第 6 個任務來的時候,此線程池就“忙”不過來了。
3.3)自定義拒絕策略
自定義拒絕策略只需要新建一個 RejectedExecutionHandler 對象,然後重寫它的 rejectedExecution() 方法即可,如下代碼所示:
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 3, 10,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(2),
new RejectedExecutionHandler() { // 添加自定義拒絕策略
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 業務處理方法
System.out.println("執行自定義拒絕策略");
}
});
for (int i = 0; i < 6; i++) {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName());
});
}
以上代碼執行的結果如下:
執行自定義拒絕策略
pool-1-thread-2
pool-1-thread-3
pool-1-thread-1
pool-1-thread-1
pool-1-thread-2
可以看出線程池執行了自定義的拒絕策略,我們可以在 rejectedExecution 中添加自己業務處理的代碼。
3.4)ThreadPoolExecutor 擴展
ThreadPoolExecutor 的擴展主要是通過重寫它的 beforeExecute() 和 afterExecute() 方法實現的,我們可以在擴展方法中添加日誌或者實現數據統計,比如統計線程的執行時間,如下代碼所示:
public class ThreadPoolExtend {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 線程池擴展調用
MyThreadPoolExecutor executor = new MyThreadPoolExecutor(2, 4, 10,
TimeUnit.SECONDS, new LinkedBlockingQueue());
for (int i = 0; i < 3; i++) {
executor.execute(() -> {
Thread.currentThread().getName();
});
}
}
/**
* 線程池擴展
*/
static class MyThreadPoolExecutor extends ThreadPoolExecutor {
// 保存線程執行開始時間
private final ThreadLocal<Long> localTime = new ThreadLocal<>();
public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
/**
* 開始執行之前
* @param t 線程
* @param r 任務
*/
@Override
protected void beforeExecute(Thread t, Runnable r) {
Long sTime = System.nanoTime(); // 開始時間 (單位:納秒)
localTime.set(sTime);
System.out.println(String.format("%s | before | time=%s",
t.getName(), sTime));
super.beforeExecute(t, r);
}
/**
* 執行完成之後
* @param r 任務
* @param t 拋出的異常
*/
@Override
protected void afterExecute(Runnable r, Throwable t) {
Long eTime = System.nanoTime(); // 結束時間 (單位:納秒)
Long totalTime = eTime - localTime.get(); // 執行總時間
System.out.println(String.format("%s | after | time=%s | 耗時:%s 毫秒",
Thread.currentThread().getName(), eTime, (totalTime / 1000000.0)));
super.afterExecute(r, t);
}
}
}
以上程序的執行結果如下所示:
pool-1-thread-1 | before | time=4570298843700
pool-1-thread-2 | before | time=4570298840000
pool-1-thread-1 | after | time=4570327059500 | 耗時:28.2158 毫秒
pool-1-thread-2 | after | time=4570327138100 | 耗時:28.2981 毫秒
pool-1-thread-1 | before | time=4570328467800
pool-1-thread-1 | after | time=4570328636800 | 耗時:0.169 毫秒
4)總結
線程池的使用必須要通過 ThreadPoolExecutor 的方式來創建,這樣纔可以更加明確線程池的運行規則,規避資源耗盡的風險。
當線程池的任務隊列沒有可用空間且線程池的線程數量已經達到了最大線程數時,則會執行拒絕策略,Java 自動的拒絕策略有 4 種,用戶也可以通過重寫 rejectedExecution() 來自定義拒絕策略,我們還可以通過重寫 beforeExecute() 和 afterExecute() 來實現 ThreadPoolExecutor 的擴展功能。
——————————————————————————————————————————————
關注公衆號,回覆 【算法】,獲取高清算法書!