前言
隨著當(dāng)今處理器計算能力愈發(fā)強(qiáng)大,可用的核心數(shù)量越來越多居灯,各個應(yīng)用對其實現(xiàn)更高吞吐量的需求的不斷增長祭务,多線程 API 變得非常流行内狗。在此背景下,Java自JDK1.5 提供了自己的多線程框架义锥,稱為 Executor 框架.
1. Executor 框架是什么柳沙?
1.1 簡介
Java Doc中是這么描述的
An object that executes submitted
Runnable
tasks. This interface provides a way of decoupling task submission from the mechanics of how each task will be run, including details of thread use, scheduling, etc. AnExecutor
is normally used instead of explicitly creating threads.執(zhí)行提交的Runnable任務(wù)的對象。這個接口提供了一種將任務(wù)提交與如何運行每個任務(wù)的機(jī)制拌倍,包括線程的詳細(xì)信息使用赂鲤、調(diào)度等。通常使用Executor而不是顯式地創(chuàng)建線程柱恤。
我們可以這么理解:Executor就是一個線程池框架数初,在開發(fā)中如果需要創(chuàng)建線程可優(yōu)先考慮使用Executor,無論你需要多線程還是單線程梗顺,Executor為你提供了很多其他功能泡孩,包括線程狀態(tài),生命周期的管理寺谤。
Executor 位于java.util.concurrent.Executors
仑鸥,提供了用于創(chuàng)建工作線程的線程池的工廠方法。它包含一組用于有效管理工作線程的組件变屁。Executor API 通過 Executors
將任務(wù)的執(zhí)行與要執(zhí)行的實際任務(wù)解耦锈候。 這是 生產(chǎn)者-消費者
模式的一種實現(xiàn)。
浮現(xiàn)于腦海中的一個基本的問題是敞贡,當(dāng)我們創(chuàng)建 java.lang.Thread
對象或調(diào)用實現(xiàn)了 Runnable
/Callable
接口來實現(xiàn)多線程時,為什么需要線程池摄职?
如果我們不采用線程池誊役,為每一個請求都創(chuàng)建一個線程的話:
- 管理線程的生命周期開銷非常高。管理這些線程的生命周期會明顯增加 CPU 的執(zhí)行時間谷市,會消耗大量計算資源蛔垢。
- 線程間上下文切換造成大量資源浪費。
- 程序穩(wěn)定性會受到影響迫悠。我們知道鹏漆,創(chuàng)建線程的數(shù)量存在一個限制,這個限制將隨著平臺的不同而不同创泄,并且受多個因素制約艺玲,包括jvm的啟動參數(shù)、Thread構(gòu)造函數(shù)中請求的棧大小鞠抑,以及底層操作的限制等饭聚。如果超過了這個限制,那么很可能拋出OutOfMemoryError異常搁拙,這對于運行中的應(yīng)用來說是非常危險的秒梳。
所有的這些因素都會導(dǎo)致系統(tǒng)吞吐量下降法绵。線程池通過保持一些存活線程并重用這些線程來克服這個問題。當(dāng)提交到線程池中的任務(wù)多于線程池最大任務(wù)數(shù)時酪碘,那些多余的任務(wù)將被放到一個隊列
中朋譬。 一旦正在執(zhí)行的線程有空閑了,它們會從隊列中取下一個任務(wù)來執(zhí)行兴垦。JDK 中的 Executors中徙赢, 此任務(wù)隊列是沒有長度限制的。
1.2 實現(xiàn)
我們先來看一下Executor的實現(xiàn)關(guān)系滑进。
還是蠻好理解的犀忱,正如Java優(yōu)秀框架的一貫設(shè)計思路,頂級接口-次級接口-虛擬實現(xiàn)類-實現(xiàn)類扶关。
Executor:執(zhí)行者阴汇,java線程池框架的最上層父接口,地位類似于spring的BeanFactry节槐、集合框架的Collection接口搀庶,在Executor這個接口中只有一個execute方法,該方法的作用是向線程池提交任務(wù)并執(zhí)行铜异。
ExecutorService:該接口繼承自Executor接口哥倔,添加了shutdown、shutdownAll揍庄、submit咆蒿、invokeAll等一系列對線程的操作方法,該接口比較重要蚂子,在使用線程池框架的時候沃测,經(jīng)常用到該接口。
AbstractExecutorService:這是一個抽象類食茎,實現(xiàn)ExecuotrService接口蒂破,
ThreadPoolExecutor:這是Java線程池最核心的一個類,該類繼承自AbstractExecutorService别渔,主要功能是創(chuàng)建線程池附迷,給任務(wù)分配線程資源,執(zhí)行任務(wù)哎媚。
ScheduledExecutorSerivce 和 ScheduledThreadPoolExecutor 提供了另一種線程池:延遲執(zhí)行和周期性執(zhí)行的線程池喇伯。
Executors:這是一個靜態(tài)工廠類,該類定義了一系列靜態(tài)工廠方法抄伍,通過這些工廠方法可以返回各種不同的線程池艘刚。
2. Executors 的類型
現(xiàn)在我們已經(jīng)了解了 Executors 是什么, 讓我們來看看不同類型的 Executors截珍。
2.1 SingleThreadExecutor
此線程池 Executor 只有一個線程攀甚。它用于以順序方式的形式執(zhí)行任務(wù)箩朴。如果此線程在執(zhí)行任務(wù)時因異常而掛掉,則會創(chuàng)建一個新線程來替換此線程秋度,后續(xù)任務(wù)將在新線程中執(zhí)行炸庞。
ExecutorService executorService = Executors.newSingleThreadExecutor()
2.2 FixedThreadPool(n)
顧名思義,它是一個擁有固定數(shù)量線程的線程池荚斯。提交給 Executor 的任務(wù)由固定的 n
個線程執(zhí)行埠居,如果有更多的任務(wù),它們存儲在 LinkedBlockingQueue
里事期。這個數(shù)字 n
通常跟底層處理器支持的線程總數(shù)有關(guān)滥壕。
ExecutorService executorService = Executors.newFixedThreadPool(4);
2.3 CachedThreadPool
該線程池主要用于執(zhí)行大量短期并行任務(wù)的場景。與固定線程池不同兽泣,此線程池的線程數(shù)不受限制绎橘。如果所有的線程都在忙于執(zhí)行任務(wù)并且又有新的任務(wù)到來了,這個線程池將創(chuàng)建一個新的線程并將其提交到 Executor唠倦。只要其中一個線程變?yōu)榭臻e称鳞,它就會執(zhí)行新的任務(wù)。 如果一個線程有 60 秒的時間都是空閑的稠鼻,它們將被結(jié)束生命周期并從緩存中刪除冈止。
但是,如果管理得不合理候齿,或者任務(wù)不是很短的熙暴,則線程池將包含大量的活動線程。這可能導(dǎo)致資源紊亂并因此導(dǎo)致性能下降慌盯。
ExecutorService executorService = Executors.newCachedThreadPool();
2.4 ScheduledExecutor
當(dāng)我們有一個需要定期運行的任務(wù)或者我們希望延遲某個任務(wù)時怨咪,就會使用此類型的 executor。
ScheduledExecutorService scheduledExecService = Executors.newScheduledThreadPool(1);
可以使用 scheduleAtFixedRate
或 scheduleWithFixedDelay
在 ScheduledExecutor
中定期的執(zhí)行任務(wù)润匙。
scheduledExecService.scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
scheduledExecService.scheduleWithFixedDelay(Runnable command, long initialDelay, long period, TimeUnit unit)
這兩種方法的主要區(qū)別在于它們對連續(xù)執(zhí)行定期任務(wù)之間的延遲的應(yīng)答。
scheduleAtFixedRate
:無論前一個任務(wù)何時結(jié)束唉匾,都以固定間隔執(zhí)行任務(wù)孕讳。
scheduleWithFixedDelay
:只有在當(dāng)前任務(wù)完成后才會啟動延遲倒計時。
3. 對 Future 對象的理解
由于提交給Executor 的任務(wù)是異步的巍膘,需要有一個對象來接收Executor 的處理結(jié)果厂财,這個對象就是java.util.concurrent.Future
(類似于JS中的Promise)。
應(yīng)用方式:
Future<String> result = executorService.submit(callableTask);
調(diào)用者可以繼續(xù)執(zhí)行主程序峡懈,當(dāng)需要提交任務(wù)的結(jié)果時璃饱,他可以在這個 Future
對象上調(diào)用.get()
方法來獲取。如果任務(wù)完成肪康,結(jié)果將立即返回給調(diào)用者荚恶,否則調(diào)用者將被阻塞撩穿,直到 Executor 完成此操作的執(zhí)行并計算出結(jié)果。(了解JS的童鞋此處可以和Promise的then()相類比)谒撼。
如果調(diào)用者不能無限期地等待任務(wù)執(zhí)行的結(jié)果食寡,那么這個等待時間也可以設(shè)置為定時地±保可以通過 Future.get(long timeout抵皱,TimeUnit unit)
方法實現(xiàn),如果在規(guī)定的時間范圍內(nèi)沒有返回結(jié)果辩蛋,則拋出 TimeoutException
呻畸。調(diào)用者可以處理此異常并繼續(xù)執(zhí)行該程序。
如果在執(zhí)行任務(wù)時出現(xiàn)異常悼院,則對 get 方法的調(diào)用將拋出一個ExecutionException
伤为。
對于 Future.get()
方法返回的結(jié)果,一個重要的事情是樱蛤,只有提交的任務(wù)實現(xiàn)了java.util.concurrent.Callable
接口時才返回 Future
钮呀。如果任務(wù)實現(xiàn)了Runnable
接口,那么一旦任務(wù)完成昨凡,對 .get()
方法的調(diào)用將返回 null
爽醋。
另一點是 Future.cancel(boolean mayInterruptIfRunning)
方法。此方法用于取消已提交任務(wù)的執(zhí)行便脊。如果任務(wù)已在執(zhí)行蚂四,則 Executor 將嘗試在mayInterruptIfRunning
標(biāo)志為 true
時中斷任務(wù)執(zhí)行。
4. Example: 創(chuàng)建和執(zhí)行一個簡單的 Executor
我們現(xiàn)在將創(chuàng)建一個任務(wù)并嘗試在 fixed pool Executor 中執(zhí)行它:
public class Task implements Callable<String> {
private String message;
public Task(String message) {
this.message = message;
}
@Override
public String call() throws Exception {
return "Hello " + message + "!";
}
}
Task
類實現(xiàn) Callable
接口并有一個 String
類型作為返回值的方法哪痰。 這個方法也可以拋出 Exception
遂赠。這種向 Executor 拋出異常的能力以及 Executor 將此異常返回給調(diào)用者的能力非常重要,因為它有助于調(diào)用者知道任務(wù)執(zhí)行的狀態(tài)晌杰。
現(xiàn)在讓我們來執(zhí)行一下這個任務(wù):
public class ExecutorExample {
public static void main(String[] args) {
Task task = new Task("World");
ExecutorService executorService = Executors.newFixedThreadPool(4);
Future<String> result = executorService.submit(task);
try {
System.out.println(result.get());
} catch (InterruptedException | ExecutionException e) {
System.out.println("Error occured while executing the submitted task");
e.printStackTrace();
}
executorService.shutdown();
}
}
我們創(chuàng)建了一個具有4個線程數(shù)的 FixedThreadPool
Executors跷睦,并實例化了 Task
類,并將它提交給 Executors 執(zhí)行肋演。 結(jié)果由 Future
對象返回抑诸,然后我們在屏幕上打印。
讓我們運行 ExecutorExample
并查看其輸出:
Hello World!
最后爹殊,我們調(diào)用 executorService
對象上的 shutdown 來終止所有線程并將資源返回給 OS蜕乡。
shutdown()
方法等待 Executor 完成當(dāng)前提交的任務(wù)。 但是梗夸,如果要求是立即關(guān)閉 Executor 而不等待层玲,那么我們可以使用 shutdownNow()
方法。
任何待執(zhí)行的任務(wù)都將結(jié)果返回到 java.util.List
對象中。
我們也可以通過實現(xiàn) Runnable
接口來創(chuàng)建同樣的任務(wù):
public class Task implements Runnable{
private String message;
public Task(String message) {
this.message = message;
}
public void run() {
System.out.println("Hello " + message + "!");
}
}
當(dāng)我們實現(xiàn) Runnable 時辛块,這里有一些重要的變化畔派。
- 無法從
run()
方法得到任務(wù)執(zhí)行的結(jié)果。 因此憨降,我們直接在這里打印父虑。 -
run()
方法不可拋出任何已受檢的異常。
Notes:如何合理配置線程池的大小
一般需要根據(jù)任務(wù)的類型來配置線程池大惺谝:
如果是CPU密集型任務(wù)士嚎,就需要盡量壓榨CPU,參考值可以設(shè)為 NCPU+1
如果是IO密集型任務(wù)悔叽,參考值可以設(shè)置為2*NCPU
當(dāng)然莱衩,這只是一個參考值,具體的設(shè)置還需要根據(jù)實際情況進(jìn)行調(diào)整娇澎,比如可以先將線程池大小設(shè)置為參考值笨蚁,再觀察任務(wù)運行情況和系統(tǒng)負(fù)載、資源利用率來進(jìn)行適當(dāng)調(diào)整趟庄。
您的點贊與支持是作者寫作的最大動力括细!