帶著問題閱讀
1、什麼是池化,池化能帶來什麼好處
2、如何設計一個資源池
3、Java的執行緒池如何使用,Java提供了哪些內建執行緒池
4、執行緒池使用有哪些注意事項
池化技術
池化思想介紹
池化思想是將重量級資源預先準備好,在使用時可重複使用這些預先準備好的資源。
池化思想的核心概念有:
- 資源建立/銷燬開銷大
- 提前建立,集中管理
- 重複利用,資源可回收
例如大街上的共享單車,使用者掃碼開鎖,使用完後歸還到停放點,下一個使用者可以繼續使用,共享單車由廠商統一管理,為使用者節省了購買單車的開銷。
池化技術的應用
常見的池化技術應用有:資源池、連線池、執行緒池等。
-
資源池
在各種電商平臺大促活動時,平臺需要支撐平時幾十倍的流量,因此各大平臺在需要提前準備大量伺服器進行擴容,在活動完畢以後,擴容的伺服器資源又白白浪費。將計算資源池化,在業務高峰前進行分配,高峰結束後提供給其他業務或使用者使用,即可節省大量消耗,資源池化也是雲端計算的核心技術之一。
-
連線池
網路連線的建立和釋放也是一個開銷較大的過程,提前在伺服器之間建立好連線,在需要使用的時候從連線池中獲取,使用完畢後歸還連線池,以供其他請求使用,以此可節省掉大量的網路連線時間,如資料庫連線池、HttpClient連線池。
-
執行緒池
執行緒的建立銷燬都涉及到核心態切換,提前建立若干數量的執行緒提供給客戶端複用,可節約大量的CPU消耗以便處理業務邏輯。執行緒池也是接下來重點要講的內容。
如何設計一個執行緒池
設計一個執行緒池,至少需要提供的核心能力有:
- 執行緒池容器:用於容納初始化時預先建立的執行緒。
- 執行緒狀態管理:管理池內執行緒的生命週期,記錄每個執行緒當前的可服務狀態。
- 執行緒請求管理:對呼叫端提供獲取和歸還執行緒的介面。
- 執行緒耗盡策略:提供策略以處理執行緒耗盡問題,如拒絕服務、擴容執行緒池、排隊等待等。
基於以上角度,我們來分析Java是如何設計執行緒池功能的。
Java執行緒池解析
ThreadPoolExecutor使用介紹
大象裝冰箱總共分幾步
// 1.建立執行緒池
ThreadPoolExecutor threadPool =
new ThreadPoolExecutor(1, 1, 1L, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
// 2.提交任務
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("task running");
}
}});
// 3.關閉執行緒池
threadPool.shutDown();
Java通過ThreadPoolExecutor
提供執行緒池的實現,如示例程式碼,初始化一個容量為1的執行緒池、然後提交任務、最後關閉執行緒池。
ThreadPoolExecutor
的核心方法主要有
-
建構函式:
ThreadPoolExecutor
提供了多個建構函式,以下對基礎建構函式進行說明。public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
-
corePoolSize:執行緒池的核心執行緒數。池內執行緒數小於
corePoolSize
時,執行緒池會建立新執行緒執行任務。 -
maximumPoolSize:執行緒池的最大執行緒數。池內執行緒數大於
corePoolSize
且workQueue
任務等待佇列已滿時,執行緒池會建立新執行緒執行佇列中的任務,直到執行緒數達到maximumPoolSize
為止。 -
keepAliveTime:非核心執行緒的存活時長。池內超過
corePoolSize
數量的執行緒可存活的時長。 -
unit:非核心執行緒存活時長單位。與
keepAliveTime
取值配合,如示例程式碼表示1分鐘。 -
workQueue:任務提交佇列。當無空閒核心執行緒時,儲存待執行任務。
型別 作用 ArrayBlockingQueue 陣列結構的有界阻塞佇列 LinkedBlockingQueue 連結串列結構的阻塞佇列,可設定是否有界 SynchronousQueue 不儲存元素的阻塞佇列,直接將任務提交給執行緒池執行 PriorityBlockingQueue 支援優先順序的無界阻塞佇列 DelayQueue 支援延時執行的無界阻塞佇列 -
threadFactory:執行緒工廠。用於建立執行緒物件。
-
handler:拒絕策略。執行緒池執行緒數量達到
maximumPoolSize
且workQueue
已滿時的處理策略。型別 作用 AbortPolicy 拒絕並丟擲異常。預設 CallerRunsPolicy 由提交任務的執行緒執行任務 DiscardOldestPolicy 拋棄佇列頭部任務 DiscardPolicy 拋棄該任務
-
-
執行函式:
execute
和submit
,主要分別用於執行Runnable
和Callable
。// 提交Runnable void execute(Runnable command); // 提交Callable並返回Future <T> Future<T> submit(Callable<T> task); // 提交Runnable,執行結束後Future.get會返回result <T> Future<T> submit(Runnable task, T result); // 提交Runnable,執行結束後Future.get會返回null Future<?> submit(Runnable task);
-
停止函式:
shutDown
和shutDownNow
。// 不再接收新任務,等待剩餘任務執行完畢後停止執行緒池 void shutdown(); // 不再接收新任務,並嘗試中斷執行中的任務,返回還在等待佇列中的任務列表 List<Runnable> shutdownNow();
內建執行緒池使用
To be useful across a wide range of contexts, this class provieds many adjustable parameters and extensibility hooks. However, programmers are urged to use the more convenient {@link Executors} factory methods {@link Executors#newCachedThreadPool} (unbounded thread poll, with automatic thread reclamation), {@link Executors#newFixedThreadPool} (fixed size thread pool) and {@link Executors#newSingleThreadExecutor}(single background thread), that preconfigure settings for the most common usage scenarios.
由於ThreadPoolExecutor
引數複雜,Java提供了三種內建執行緒池newCachedThreadPool
、newFixedThreadPool
和newSingleThreadExecutor
應對大多數場景。
-
Executors.newCachedThreadPool()
無界執行緒池,核心執行緒池大小為0,最大為Integer.MAX_VALUE
,因此嚴格來講並不算無界。採用SynchronousQueue
作workQueue
,意味著任務不會被阻塞儲存在佇列,而是直接遞交到執行緒池,如執行緒池無可用執行緒,則建立新執行緒執行。public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
-
Executors.newFixedThreadPool(int nThreads)
固定大小執行緒池,其中coreSize
和maxSize
相等,且過期時間為0,表示經過一定數量任務提交後,執行緒池將始終維持在nThreads
數量大小,不會新增也不會回收執行緒。public static ExecutorService new FixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
-
Executors.newSingleThreadExecutor()
單執行緒池,引數與fixedThreadPool
類似,只是將數量限制在1,單執行緒池主要避免重複建立銷燬執行緒物件,也可用於序列化執行任務。不同與其他執行緒池,單執行緒池採用FinallizableDelegatedExecutorService
對ThreadPoolExecutor
物件進行包裝,感興趣的同學可以看下原始碼,其方法實現僅僅是對被包裝物件方法的直接呼叫。包裝物件主要用於避免使用者將執行緒池強制轉換為ThreadPoolExecutor
來修改執行緒池大小。public static ExecutorService newSingleThreadExecutor() { return new FinallizableDelegatedExecutorService( (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockQueue<Runnable>())) ); }
ThreadPoolExecutor解析
整體設計
ThreadPoolExecutor
基於ExecutorService
介面實現提交任務,未採取常規資源池獲取/歸還資源的形式,整個執行緒池和執行緒的生命週期都由ThreadPoolExecutor
進行管理,執行緒物件不對外暴露;ThreadPoolExecutor
的任務管理機制類似於生產者消費者模型,其內部維護一個任務佇列和消費者,一般情況下,任務被提交到佇列中,消費執行緒從佇列中拉取任務並將其執行。
執行緒池生命週期
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
private static int runStateOf(int c) { return c & ~CAPACITY; } //計算當前執行狀態
private static int workerCountOf(int c) { return c & CAPACITY; } //計算當前執行緒數量
private static int ctlOf(int rs, int wc) { return rs | wc; } //通過狀態和執行緒數生成ctl
TreadPoolExecutor
通過ctl
維護執行緒池的狀態和執行緒數量,其中高3位儲存執行狀態,低29位儲存執行緒數量。
位運算操作推薦參考第三篇文章。
執行緒池設定了RUNNING
、SHUTDOWN
、STOP
、TIDYING
和TERMINATED
五種狀態,其轉移圖如下:
在這5種狀態中,只有RUNNING
時執行緒池可接收新任務,其餘4種狀態在呼叫shutDown
或shutDownNow
後觸發轉換,且在這4種狀態時,執行緒池均不再接收新任務。
任務管理解析
// 用於存放提交任務的佇列
private final BlockingQueue<Runnable> workQueue;
// 用於儲存池內的工作執行緒,Java將Thread包裝成Worker儲存
private final HashSet<Worder> workers = new HashSet<Worker>();
ThreadPoolExecutor
主要通過workQueue
和workers
兩個欄位用於管理和執行任務。
執行緒池任務執行流程如圖,結合ThreadPoolExecutor.execute
原始碼,對任務執行流程進行說明:
-
當任務提交到執行緒池時,如果當前執行緒數量小於核心執行緒數,則會將為該任務直接建立一個
worker
並將任務交由worker
執行。if (workerCountOf(c) < corePoolSize) { // 建立新worker執行任務,true表示核心執行緒 if (addWorker(command, true)) return; c = ctl.get(); }
-
當已經達到核心執行緒數後,任務會提交到佇列儲存;
// 放入workQueue佇列 if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); // 這裡採用double check再次檢測執行緒池狀態 if (! isRunning(recheck) && remove(command)) reject(command); // 避免加入佇列後,所有worker都已被回收無可用執行緒 else if (workerCountOf(recheck) == 0) addWorker(null, false); }
-
如果佇列已滿,則依據最大執行緒數量建立新
worker
執行。如果新增worker
失敗,則依據設定策略拒絕任務。// 接上,放入佇列失敗 // 新增新worker執行任務,false表示非核心執行緒 else if (!addWorker(command, false)) // 如新增失敗,執行拒絕策略 reject(command);
woker物件
ThreadPoolExecutor
沒有直接使用Thread
記錄執行緒,而是定義了worker
用於包裝執行緒物件。
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
...
final Thread thread;
Runnable firstTask;
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
// worker物件被建立後就會執行
public void run() {
runWorker(this);
}
}
worker
物件通過addWorker
方法建立,一般會為其指定一個初始任務firstTask
,當worker
執行完畢以後,worker
會從阻塞佇列中讀取任務,如果沒有任務,則該worker
會陷入阻塞狀態給出worker
的核心邏輯程式碼:
private boolean addWorker(Runnable firstTask, boolean core) {
...
// 指定firstTask,可能為null
w = new Worker(firstTask);
...
if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
workerAdded = true;
}
...
// 執行新新增的worker
if (workerAdded) {
t.start();
workerStarted = true;
}
}
final void runWorker(Worker w) {
// 等待workQueue的任務
while (task != null || (task = getTask()) != null) {
...
}
}
private Runnable getTask() {
...
for (;;) {
...
// 如果是普通工作執行緒,則根據執行緒存活時間讀取阻塞佇列
// 如果是核心工作執行緒,則直接陷入阻塞狀態,等待workQueue獲取任務
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
...
}
}
如下圖,任務提交後觸發addWorker
建立worker
物件,該物件執行任務完畢後,則迴圈獲取佇列中任務等待執行。
Java執行緒池實踐建議
不建議使用Exectuors
執行緒池不允許使用
Executors
去建立,而是通過ThreadPoolExecutor
的方式,這樣的處理方式讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險。《阿里巴巴開發手冊》
雖然Java推薦開發者直接使用Executors
提供的執行緒池,但實際開發中通常不使用。主要考慮問題有:
-
潛在的OOM問題
CachedThreadPool
將最大數量設定為Integer.MAX_VALUE
,如果一直提交任務,可能造成Thread
物件過多引起OOM
。FixedThreadPool
和SingleThreadPoo
的佇列LinkedBlockingQueue
無容量限制,阻塞任務過多也可能造成OOM
。 -
執行緒問題定位不便
由於未指定
ThreadFactory
,執行緒名稱預設為pool-poolNumber-thread-thredNumber
,執行緒出現問題後不便定位具體執行緒池。 -
執行緒池分散
通常在完善的專案中,由於執行緒是重量資源,因此執行緒池由統一模組管理,重複建立執行緒池容易造成資源分散,難以管理。
執行緒池大小設定
通常按照IO繁忙型和CPU繁忙型任務分別採用以下兩個普遍公式。
在理論場景中,如一個任務IO耗時40ms,CPU耗時10ms,那麼在IO處理期間,CPU是空閒的,此時還可以處理4個任務(40/10),因此理論上可以按照IO和CPU的時間消耗比設定執行緒池大小。
《JAVA併發程式設計實踐》中還考慮數量乘以目標CPU的利用率
在實際場景中,我們通常無法準確測算IO和CPU的耗時佔比,並且隨著流量變化,任務的耗時佔比也不能固定。因此可根據業務需求,開設執行緒池運維介面,根據線上指標動態調整執行緒池引數。
推薦參考第二篇美團執行緒池應用
執行緒池監控
ThreadPoolExecutor
提供以下方法監控執行緒池:
-
getTaskCount()
返回被排程過的任務數量 -
getCompletedTaskCount()
返回完成的任務數量 -
getPoolSize()
返回當前執行緒池執行緒數量 -
getActiveCount()
返回活躍執行緒數量 -
getQueue()
獲取佇列,一般用於監控阻塞任務數量和佇列空間大小
參考
-
《Java併發程式設計實踐》