最近精讀Netty原始碼,讀到NioEventLoop部分的時候,發現對Java執行緒&執行緒池有些概念還有困惑, 所以深入總結一下
Java執行緒池一:執行緒基礎
Java執行緒池二:執行緒池原理
為什麼需要使用執行緒池
Java執行緒對映的是系統核心執行緒,是稀缺資源,使用執行緒池主要有以下幾點好處
- 降低資源消耗:重複利用池中執行緒降低執行緒的建立和消耗造成的資源消耗。
- 提高響應速度:任務到達時直接使用池總中空閒的執行緒,可以不用等待執行緒建立。
- 提高執行緒的可管理性:執行緒是稀缺資源,不能無限制建立,使用執行緒池可以統一進行分配、監控、調優。
執行緒池框架簡介
- Executor介面:提供
execute
方法提交任務 - ExecutorService介面:提供可以跟蹤任務執行結果的
submit
方法 & 提供執行緒池關閉的方法(shutdown, shutdowNow) - AbstractExecutorService抽象類:實現submit方法
- ThreadPoolExecutor: 執行緒池實現類
- ScheduleThreadPoolExecutor:可以執行定時任務的執行緒池
ThreadPoolExecutor原理
核心引數以及含義
- corePoolSize:核心執行緒池大小
- maximumPoolSize: 執行緒池最大大小
- workQueue: 工作佇列(任務暫時存放的地方)
- RejectedExecutionHandler:拒絕策略(執行緒池無法執行該任務時的處理策略)
任務提交流程
任務提交過程見下流程圖
執行緒池的狀態
- RUNNING:正常的執行緒池執行狀態
- SHUTDOWN:呼叫shutdown方法到該狀態,該狀態下拒絕提交新任務,但會將已提交的任務的處理完畢
- STOP:呼叫shutdownNow方法到該狀態,該狀態下拒絕新任務的提交 & 丟棄工作佇列中的任務 & 中斷正在執行任務的工作執行緒
- TIDYING:工作佇列和執行緒池都為空時自動到該狀態
- TERMINATED:terminated方法返回之後自動到該狀態
工作佇列
核心執行緒池滿時,任務會嘗試提交到工作佇列,後續工作執行緒會從工作佇列中獲取任務執行。
因為涉及到多個執行緒對工作佇列的讀寫,所以工作佇列需要是執行緒安全的,Java提供了以下幾種執行緒安全的佇列(BlockingQueue)
實現類 | 工作機制 |
---|---|
ArrayBlockingQueue | 底層實現是陣列 |
LinkedBlockingDeque | 底層實現是連結串列 |
PriorityBlockingQueue | 優先佇列,本質是個小頂堆 |
DelayQueue | 延時佇列 (優先佇列 & 元素實現Delayed介面),ScheduledThreadPoolExecutor實現的關鍵 |
SynchronousQueue | 同步佇列 |
BlockingQueue 多組讀寫操作API
操作 | 描述 |
---|---|
add/remove | 佇列已滿/佇列已空時,丟擲異常 |
put/take | 佇列已滿/佇列已空時,阻塞等待 |
offer/poll | 佇列已滿/佇列已空時,返回特殊值(false/null) |
offer(time) / poll(time) | 超時時間內無法寫入或者讀取成功,返回特殊值 |
拒絕策略
拒絕策略是當執行緒池滿負載時(任務佇列已滿 & 執行緒池已滿)對新提交任務的處理策略,jdk提供瞭如下四種實現,其中AbortPolicy是預設實現。
實現類 | 工作機制 |
---|---|
AbortPolicy | 丟擲RejectedExecutionException異常 |
CallerRunsPolicy | 呼叫執行緒執行該任務 |
DiscardOldestPolicy | 丟棄工作佇列頭部任務,再嘗試提交該任務 |
DiscardPolicy | 直接丟棄 |
當然我們可以有自定義的實現,比如記錄日誌、任務例項持久化,同時傳送報警到開發人員。
跟蹤任務的執行結果
執行緒池提供了幾個submit方法, 呼叫執行緒可以根據返回的Future物件獲取任務執行結果,那麼它的實現原理又是什麼吶?
裝飾模式對task的run方法進行增強
1.提交任務前,會把task裝飾成一個FutureTask物件
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
2.FutureTask物件的run方法會儲存返回的結果或者異常。呼叫方可以根據FutureTask獲取任務的執行結果。
//省略了部分程式碼
public void run() {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
//執行任務
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
//儲存異常
setException(ex);
}
if (ran)
//儲存返回值
set(result);
}
執行緒池的關閉
shutdown
shutdown將執行緒池的狀態設定成SHUTDOWN,同時拒絕提交新的任務,但是已提交的任務會正常執行
shutdownNow
shutdownNow將執行緒池的狀態設定成STOP,該狀態下拒絕提交新的任務 & 丟棄工作佇列中的任務& 中斷當前活躍的執行緒(嘗試停止正在執行的任務)
需要注意的是shutdownNow對於正在執行的任務只是嘗試停止
,不保證成功(取決於任務是否監聽處理中斷位)
ScheduledThreadPoolExecutor 定時排程原理
ScheduledThreadPoolExecutor在ThreadPoolExecutor之上擴充套件實現了定時排程的能力
1.例項化時工作佇列使用延時佇列(DelayedWorkQueue)--- 本質是個小頂堆
public ScheduledThreadPoolExecutor(int corePoolSize,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), handler);
}
2.提交的任務裝飾成ScheduledFutureTask型別,並把任務加入到工作佇列(不直接呼叫execute)
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
//裝飾
RunnableScheduledFuture<?> t = decorateTask(command,
new ScheduledFutureTask<Void>(command, null,
triggerTime(delay, unit)));
//任務加入工作佇列
delayedExecute(t);
return t;
}
3.ScheduledFutureTask實現Delayed和Comparable介面
所以提交到工作佇列中的任務是按照任務執行時間排序的(最早執行的任務在頭部),因為工作佇列是個小頂堆。
public long getDelay(TimeUnit unit) {
return unit.convert(time - now(), NANOSECONDS);
}
public int compareTo(Delayed other) {
if (other == this) // compare zero if same object
return 0;
if (other instanceof ScheduledFutureTask) {
ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
long diff = time - x.time;
if (diff < 0)
return -1;
else if (diff > 0)
return 1;
else if (sequenceNumber < x.sequenceNumber)
return -1;
else
return 1;
}
long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
}
4.只能從工作佇列中獲取已到執行時間的任務
public RunnableScheduledFuture<?> poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
RunnableScheduledFuture<?> first = queue[0];
//如果頭部的任務還沒有到執行時間, 直接返回null
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
return finishPoll(first);
} finally {
lock.unlock();
}
}
執行緒池配置
假設:CPU核心數是N,每個任務的執行時間是T,任務的超時時間是timeout,核心執行緒數是corePoolSize,工作佇列大小是workQueue, 最大執行緒數是 maxPoolSize, 任務最大併發數為maxTasks
核心執行緒數配置
-
對於CPU密集型任務:corePoolSize 大小設定成和CPU核心數接近,如N+1 或者 N+2
-
對於IO密集型任務:corePoolSize可以設定的比較大一些,如2N~3N;也可以通過如下邏輯進行估算
假設80%的時間是IO操作,那麼每個任務需要佔用CPU時間大概是0.2T, 每秒每個CPU核心最大可以執行的任務數為 = (1/0.2T) = 5/T;所以
理論上
80%IO的情況下corePoolSize可以設定為 5N (一個cpu可以對應5個工作執行緒)
工作佇列大小配置
工作佇列的大小取決於任務的超時時間 & 核心執行緒池的吞吐量
則 workQueue = corePoolSize * (1/T) * timeout = (corePoolSize * timeout) / T
需要注意的是: 工作佇列不能使用無界佇列。(無界佇列異常情況下可能耗盡系統資源,造成服務不可用)
最大執行緒數配置
最大執行緒數的大小取決於最大的任務併發數 & 工作佇列的大小 & 任務的執行時間
則 maxPoolSize = (maxTasks - workQueue) / T
拒絕策略配置
對於無關緊要的任務,我們可以直接丟棄;對於一些重要的任務需要對任務進行持久化,以便後續進行補償和恢復。
執行緒池監控
我們可以有個定時指令碼將執行緒池的最大執行緒數、工作佇列大小、已經執行的任務數、已經拒絕的任務數等資料推送到監控系統
這樣我們可以根據這些資料對執行緒池進行調優,也可以即使感知線上業務異常。