前言
由於系統資源是有限的,為了降低資源消耗,提高系統的效能和穩定性,引入了執行緒池對執行緒進行統一的管理和監控,本文將詳細講解執行緒池的使用、原理。
為什麼使用執行緒池
池化思想
執行緒池主要用到了池化思想,池化思想在計算機領域十分常見,主要用於減少資源浪費、提高效能等。
池化思想主要包含以下幾個方面:
一些常見的資源池包括執行緒池、資料庫連線池、物件池、快取池、連線池等。
池化思想可以提高系統的效能,因為它減少了資源的建立和銷燬次數,避免了不必要的開銷。透過池化,系統可以更好地應對高併發情況,降低資源競爭,提高響應速度。
什麼是執行緒池
根據池化思想,在一個系統中,為了避免執行緒頻繁的建立和銷燬,讓執行緒可以複用,引入了執行緒池的概念。執行緒池中,總有那麼幾個活躍執行緒。
當你需要使用執行緒時,可以從池子中隨便拿一個空閒執行緒,當完成工作時,並不急著關閉執行緒,而是將這個執行緒退回到池子,方便其他人使用。
簡單說就是,在使用執行緒池後,建立執行緒變成了從執行緒池中獲得空閒執行緒,關閉執行緒程式設計了向池子裡歸還執行緒。
大致流程如下:
在開發過程中,合理地使用執行緒池能夠帶來3個好處。
- 降低資源消耗。透過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。
- 提高響應速度。當任務到達時,任務可以不需要等到執行緒建立就能立即執行。
- 提高執行緒的可管理性。執行緒是稀缺資源,如果無限制地建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一分配、調優和監控。
要做到合理利用執行緒池,必須對其實現原理了如指掌。
執行緒池的使用
- 透過 ThreadPoolExecutor 建構函式
- 透過 Executors 類建立
透過建構函式
1.1. 入參含義
這個也是推薦使用的方法,因為透過 Executors 類建立可能會導致 OOM,如下圖阿里開發規範中的描述。
建構函式入參:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
建構函式入參含義:
1.2. 阻塞佇列
workQueue 可選的 BlockingQueue:
1.3. 拒絕策略
如下圖,上述拒絕策略均實現 RejectedExecutionHandler 介面,且為 ThreadPoolExecutor 的內部類。
若以上策略仍無法滿足實際應用需要,完全可以自已擴充套件 RejectedExecutionHandler 介面。
public interface RejectedExecutionHandler {
/**
* @param r 當前請求執行的任務
* @param executor 當前的執行緒池
*/
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
示例:
public class RejectedExecutionDemo {
public static class MyTask implements Runnable{
@Override
public void run() {
System.out.println(new Date() + ":Thread ID is" + Thread.currentThread().getId());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) throws InterruptedException {
MyTask myTask = new MyTask();
ExecutorService executorService = new ThreadPoolExecutor(5, 5,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(10),
Executors.defaultThreadFactory(),
(r, executor) -> System.out.println(r.hashCode() + "is discard")
);
for (int i = 0; i < 100; i++) {
executorService.submit(myTask);
Thread.sleep(10);
}
}
}
上述示例中,mytask 執行需要花費100毫秒,因此,必然會導致一些任務被直接丟棄。在實際應用中,我們可以將更詳細的資訊記錄到日誌中,來分析任務丟失情況和系統負載。
透過 Executors
Executors 類扮演著執行緒池工廠的角色,透過該類可以取得一個擁有定功能的執行緒池。
該類可以建立三種型別的 ThreadPoolExecutor:
- FixedThreadPool
- SingleThreadExecutor
- CachedThreadPool
2.1. FixedThreadPool
固定執行緒數的執行緒池,該執行緒池中的執行緒數量始終不變。當有一個新的任務提交時,執行緒池中若有空閒執行緒,則立即執行。若沒有,則新的任務會被暫時存在任務佇列中,待有執行緒空閒時,在處理佇列中的任務。
FixedThreadPool 使用的無界任務佇列 LinkedBlockingQueue,可能造成記憶體洩露。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
2.2. SingleThreadExecutor
只有一個工作執行緒的執行緒池,當多於 1 個任務被提交時,會存到任務佇列中。該執行緒池使用的無界任務佇列 LinkedBlockingQueue,可能造成記憶體洩露。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
2.3. CachedThreadPool
根據實際情況調整執行緒數的執行緒池,執行緒池的執行緒數量不確定,若有空閒執行緒可複用,則會優先使用。若所有執行緒均在工作,此時新的任務則會建立新的執行緒優先處理。所有執行緒在任務執行完畢後,將返回執行緒池進行復用。
corePoolSize 被設定為0,maximumPoolSize 被設定為無界,存活時間設定為 60s,空閒執行緒超過60秒後將會被
終止。極端情況執行緒建立過多,會導致記憶體洩露。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
ScheduledThreadPoolExecutor
簡介
如下圖, ScheduledThreadPoolExecutor 繼承自ThreadPoolExecutor,它主要用來定期執行任務,功能與 Timer 類似且更加強大,可以在建構函式中指定多個對應的後臺執行緒數。
使用
可透過 Executors 建立,原始碼如下:
public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1, threadFactory));
}
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
這裡的返回值是 ScheduledExecutorService,根據時間對執行緒進行排程。有三個主要方法:
public interface ScheduledExecutorService extends ExecutorService {
/**
* 給定時間對任務進行排程
*/
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
/**
* 週期性對任務進行排程
* 以第一個任務的開始時間 initialDelay + period
* 第一個任務在 initialDelay + period 執行
* 第二個任務在 initialDelay + period * 2 執行
*/
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
/**
* 週期性對任務進行排程
* 上一個任務結束後,再經過 period 時間開始執行
*/
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
}
如果任務遇到異常,那麼後續的所有子任務都會停止排程,因此,必須保證異常被及時處理,為週期性任務的穩定排程提供條件。
ForkJoinPool
fork 是開啟子程序,join 是等待,意思是分支子程序結束後才能得到結果,實際開發中,若頻繁的 fork 開啟執行緒可能嚴重影響系統效能,所以引入了 ForkJoinPool。
大致流程是,向 ForkJoinPool 執行緒池中提交一個 ForkJoinTask 任務,就是將任務分解成多個小任務,等任務全部完成後進行處理,這裡採用了分治的思想,具體我將在後續單獨展開,這裡不多做贅述。
ForkJoin 可能出現兩個問題:
- 子執行緒積累過多,可能導致系統效能嚴重下降;
- 呼叫層次過深,可能導致棧溢位。
執行緒池的任務提交
execute()
該方法用於提交不需要返回值的任務,且無法判斷任務是否被執行緒池執行成功。
原始碼見下面的執行緒池原理章節。
submit()
該方法用於提交需要返回值的任務。執行緒池會返回 Future 物件,可以判斷任務是否執行成功,還可以透過 Future 的get()方法來獲取返回值。
get()方法會阻塞當前執行緒直到任務完成,還可以設定超時時間,到時立即返回,不過這時有可能任務沒有執行完。
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
執行緒池的關閉
可以透過呼叫執行緒池的 shutdown 或 shutdownNow 方法來關閉執行緒池。
它們的原理是遍歷執行緒池中的工作執行緒,然後逐個呼叫執行緒的 interrupt() 來中斷執行緒,所以無法響應中斷的任務可能永遠無法終止。
兩種方法存在一定的區別,shutdownNow首先將執行緒池的狀態設定成 STOP,然後嘗試停止所有的正在執行或暫停任務的執行緒,並返回等待執行任務的列表。而 shutdown 只是將執行緒池的狀態設定成 SHUTDOWN 狀態,然後中斷所有沒有正在執行任務的執行緒。
只要呼叫了這兩個關閉方法中的任意一個,isShutdown方法就會返回true。當所有的任務都已關閉後,表示執行緒池關閉成功,這時呼叫isTerminaed方法會返回true。
至於應該呼叫哪一種方法來關閉執行緒池,應該由提交到執行緒池的任務特性決定,通常呼叫 shutdown 方法來關閉執行緒池,如果任務不一定要執行完,則可以呼叫 shutdownNow 方法。
執行緒池執行原理
執行原始碼
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 如果當前工作執行緒數是否小於核心執行緒數
if (workerCountOf(c) < corePoolSize) {
// 新增核心執行緒去執行任務,成功則return
if (addWorker(command, true))
return;
// 新增失敗,ctl有變化,需重新獲取
c = ctl.get();
}
// 判斷是否為RUNNING,此時核心執行緒數已滿,需加入任務佇列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 檢查若不是RUNNING則將任務從佇列移除
if (! isRunning(recheck) && remove(command))
// 執行拒絕策略
reject(command);
// 正常則新增一個非核心空執行緒,執行佇列中的任務
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 表示核心執行緒滿了,佇列也滿了,建立非核心執行緒,執行任務
else if (!addWorker(command, false))
// 最大執行緒數也滿了,走拒絕策略
reject(command);
}
流程圖
參考:
[1] 魏鵬. Java併發程式設計的藝術.
[2] 葛一鳴/郭超. 實戰Java高併發程式設計.