針對 IO 密集型的任務,我們可以針對原本的線程池做一些改造,從而可以提高任務的處理效率。
基本
在阿里巴巴泰山版java開發手冊
中有這麼一條:
線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,
這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。
那麼如果要使用 ThreadPoolExecutor ,那就先來看看構造方法中的所有入參:
corePoolSize : 核心線程數,當線程池中的線程數量爲 corePoolSize 時,即使這些線程處於空閒狀態,也不會銷燬(除非設置 allowCoreThreadTimeOut)。
maximumPoolSize : 最大線程數,線程池中允許的線程數量的最大值。
keepAliveTime : 線程空閒時間,當線程池中的線程數大於 corePoolSize 時,多餘的空閒線程將在銷燬之前等待新任務的最長時間。
workQueue : 任務隊列
unit :線程空閒時間的單位。
threadFactory :線程工廠,線程池創建線程時使用的工廠。
handler : 拒絕策略,因達到線程邊界和任務隊列滿時,針對新任務的處理方法。
這麼說可能有些難以理解,你可以結合下圖進行參考:
那麼由此我們可以知道,當大量任務被放入線程池之後,先是被核心線程執行,多餘的會被放進隊列裏,當隊列滿了之後纔會創建額外的線程進行處理,再多就會採取拒絕策略。
但這樣真的能滿足我們的所有需求嗎?
任務的分類
正常來說,我們可以把需要處理的任務按照消耗資源的不同,分爲兩種:CPU 密集型
和IO 密集型
。
CPU 密集型
既然名字裏帶有CPU
了,說明其消耗的主要資源就是 CPU 了。
具體是指那種包含大量運算、在持有的 CPU 分配的時間片上一直在執行任務、幾乎不需要依賴或等待其他任何東西。
這樣的任務,在我的理解中,處理起來其實沒有多少優化空間,因爲處理時幾乎沒有等待時間,所以一直佔有 CPU 進行執行,纔是最好的方式。
唯一能想到優化的地方,就是當單個線程累計較多任務時,其他線程能進行分擔,類似fork/join框架
的概念。
設置線程數時,針對單臺機器,最好就是有幾個 CPU ,就創建幾個線程,然後每個線程都在執行這種任務,永不停歇。
IO 密集型
和上面一樣,既然名字裏帶有IO
了,說明其消耗的主要資源就是 IO 了。
我們所接觸到的 IO ,大致可以分成兩種:磁盤 IO
和網絡 IO
。
磁盤 IO ,大多都是一些針對磁盤的讀寫操作,最常見的就是文件的讀寫,假如你的數據庫、 Redis 也是在本地的話,那麼這個也屬於磁盤 IO。
網絡 IO ,這個應該是大家更加熟悉的,我們會遇到各種網絡請求,比如 http 請求、遠程數據庫讀寫、遠程 Redis 讀寫等等。
IO 操作的特點就是需要等待,我們請求一些數據,由對方將數據寫入緩衝區
,在這段時間中,需要讀取數據的線程根本無事可做,因此可以把 CPU 時間片讓出去,直到緩衝區
寫滿。
既然這樣,IO 密集型任務其實就有很大的優化空間了(畢竟存在等待),那現有的線程池可以很好的滿足我們的需求嗎?
線程池的優化
還記得上面說的, ThreadPoolExecutor 針對多餘任務的處理,是先放到等待隊列中,當隊列塞滿後,再創建額外的線程進行處理。
假設我們的任務基本都是 IO 密集型,我們希望程序可以有更高的吞吐量,可以在更短的時間內處理更多的任務,那麼上面的 ThreadPoolExecutor 明顯是不滿足我們的需求,那該如何解決呢?
也許再來看看 ThreadPoolExecutor 的 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);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 如果不可以成功放入隊列,則創建線程
else if (!addWorker(command, false))
// 如果無法繼續創建線程,則拒絕任務
reject(command);
}
針對放入隊列的操作,如果隊列放入失敗,線程池就會選擇去創建線程了。因此,我們或許可以嘗試自定義線程池,針對 offer 操作,做一些自定義處理。
也就是將任務放入隊列時,先檢查線程池的線程數是否小於最大線程數,如果是,則拒絕放入隊列,否則,再嘗試放入隊列中。
如果你有看過 dubbo 或者 tomcat 的線程池,你會發現他們就有這樣的實現方法。
比如 dubbo 中的 TaskQueue,我們來看看它的 offer 方法:
@Override
public boolean offer(Runnable runnable) {
if (executor == null) {
throw new RejectedExecutionException("The task queue does not have executor!");
}
int currentPoolThreadSize = executor.getPoolSize();
// 如果有空閒等待的線程,則將任務放入隊列中,讓線程去處理任務
if (executor.getSubmittedTaskCount() < currentPoolThreadSize) {
return super.offer(runnable);
}
// 如果當前線程數小於最大線程數,則返回 false ,讓線程池去創建新的線程
if (currentPoolThreadSize < executor.getMaximumPoolSize()) {
return false;
}
// 否則,就將任務放入隊列中
return super.offer(runnable);
}
這樣就可以讓線程池優先新建線程了。需要注意的是,此時的隊列因爲需要根據線程池中的線程數決定是否放入任務成功,所以需要持有executor
對象,這點不要忘記奧。
總結
通過本篇文章,主要是讓大家重新瞭解了一下 ThreadPoolExecutor ,並針對高吞吐場景下如何進行局部優化。
有興趣的話可以訪問我的博客或者關注我的公衆號,說不定會有意外的驚喜。
https://death00.github.io/
公衆號:健程之道
點擊此處留言