六種常見線程池介紹,超詳細!

前言
在之前的文章 線程池使用及源碼分析 中有提到過一部分,本章再進行詳細的介紹,6 種常見的線程池如下:
FixedThreadPool
CachedThreadPool
ScheduledThreadPool
SingleThreadExecutor
SingleThreadScheduledExecutor
ForkJoinPool
1.FixedThreadPool
構(gòu)造函數(shù)如下:
參數(shù) nThreads:the number of threads in the pool
public?static?ExecutorService newFixedThreadPool(int?nThreads)?{
????????return?new?ThreadPoolExecutor(nThreads, nThreads,
??????????????????????????????????????0L, TimeUnit.MILLISECONDS,
??????????????????????????????????????new?LinkedBlockingQueue());
????} 核心線程數(shù)和最大線程一樣,都是 nThreads,可以將它看成是固定線程數(shù)的線程池,就算任務(wù)數(shù)超過了任務(wù)隊列(workQueue)的最大限制,也不會創(chuàng)建新的線程來進行處理,而是會采取拒絕策略。
2.CachedThreadPool
構(gòu)造函數(shù)如下:
public?static?ExecutorService newCachedThreadPool()?{
????????return?new?ThreadPoolExecutor(0, Integer.MAX_VALUE,
??????????????????????????????????????60L, TimeUnit.SECONDS,
??????????????????????????????????????new?SynchronousQueue());
????} 緩存線程池,它的特點在于線程數(shù)是幾乎可以無限增加的(實際最大可以達到 Integer.MAX_VALUE,為 231 -1 ,這個數(shù)非常大,所以基本不可能達到)。
而當(dāng)線程閑置時還可以對線程進行回收,60秒后自動進行回收。也就是說該線程池的線程數(shù)量不是固定不變的,當(dāng)然它也有一個用于存儲提交任務(wù)的隊列,但這個隊列是 SynchronousQueue,隊列的容量為0,實際不存儲任何任務(wù),它只負責(zé)對任務(wù)進行中轉(zhuǎn)和傳遞,所以效率比較高。
示例:
public?class?CachedThreadPoolDemo?{
???static?ExecutorService executorService = Executors.newCachedThreadPool();//伸縮性,60s后回收
????public?static?void?main(String[] args) {
????????for?(int?i = 0; i < 100; i++) {
????????????executorService.execute(()->{
????????????????System.out.println(Thread.currentThread().getName()+"執(zhí)行");
????????????});
????????}
????}
}執(zhí)行結(jié)果
pool-1-thread-1執(zhí)行
pool-1-thread-3執(zhí)行
pool-1-thread-2執(zhí)行
pool-1-thread-4執(zhí)行
pool-1-thread-5執(zhí)行
pool-1-thread-6執(zhí)行
pool-1-thread-7執(zhí)行
pool-1-thread-8執(zhí)行
pool-1-thread-9執(zhí)行
pool-1-thread-10執(zhí)行
pool-1-thread-11執(zhí)行
pool-1-thread-12執(zhí)行
pool-1-thread-13執(zhí)行
pool-1-thread-14執(zhí)行
...循環(huán)提交 100 個任務(wù)給線程池執(zhí)行,每個任務(wù)執(zhí)行100毫秒,因為 for 循環(huán)執(zhí)行是非??斓?,導(dǎo)致第一個任務(wù)還沒有執(zhí)行完,那么線程池會繼續(xù)創(chuàng)建線程來執(zhí)行后續(xù)提交的任務(wù)。
而當(dāng)任務(wù)執(zhí)行完之后,假設(shè)沒有新的任務(wù)了,那么大量的閑置線程又會造成內(nèi)存資源的浪費,這時線程池就會檢測線程在 60 秒內(nèi)有沒有可執(zhí)行任務(wù),如果沒有就會被銷毀,最終線程數(shù)量會減為 0。
3.ScheduledThreadPool
它支持定時或周期性執(zhí)行任務(wù)。比如每隔 10 秒鐘執(zhí)行一次任務(wù),而實現(xiàn)這種功能的方法主要有 3 種,如代碼所示:
service.schedule(new?Runnable(), 1, TimeUnit.SECONDS);
service.scheduleAtFixedRate(new?Runnable(), 1, 1, TimeUnit.SECONDS);
service.scheduleWithFixedDelay(new?Runnable(), 1, 1, TimeUnit.SECONDS);三種方法的區(qū)別
第一種方法 schedule 比較簡單,表示延遲指定時間后執(zhí)行一次任務(wù),如果代碼中設(shè)置參數(shù)為 1 秒,也就是 1 秒后執(zhí)行一次任務(wù)后就結(jié)束。
第二種方法 scheduleAtFixedRate 表示以固定的頻率執(zhí)行任務(wù),它的第二個參數(shù) initialDelay 表示第一次延時時間,第三個參數(shù) period 表示周期,也就是第一次延時后每次延時多長時間執(zhí)行一次任務(wù)。
第三種方法 scheduleWithFixedDelay 與第二種方法類似,也是周期執(zhí)行任務(wù),區(qū)別在于對周期的定義,之前的 scheduleAtFixedRate 是以任務(wù)開始的時間為時間起點開始計時,時間到就開始執(zhí)行第二次任務(wù),而不管任務(wù)需要花多久執(zhí)行;而 scheduleWithFixedDelay 方法以任務(wù)結(jié)束的時間為下一次循環(huán)的時間起點開始計時。
前面兩種測試
public?class?ScheduledThreadPoolDemo?{
????static?ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
????public?static?void?main(String[] args) throws InterruptedException {
????????service.schedule(() -> {
????????????System.out.println("定時線程執(zhí)行1:"?+ LocalDateTime.now());
????????}, 1, TimeUnit.SECONDS);
????????service.scheduleAtFixedRate(() -> {
????????????System.out.println("定時線程執(zhí)行2:"?+ LocalDateTime.now());
????????????try?{
????????????????Thread.sleep(1000);
????????????} catch?(InterruptedException e) {
????????????????e.printStackTrace();
????????????}
????????}, 1, 1, TimeUnit.SECONDS);
????}
}執(zhí)行結(jié)果
定時線程執(zhí)行2:2020-06-24T11:27:02.726
定時線程執(zhí)行1:2020-06-24T11:27:02.726
定時線程執(zhí)行2:2020-06-24T11:27:03.699
定時線程執(zhí)行2:2020-06-24T11:27:04.699
定時線程執(zhí)行2:2020-06-24T11:27:05.700
定時線程執(zhí)行2:2020-06-24T11:27:06.700
定時線程執(zhí)行2:2020-06-24T11:27:07.699可以看到第一種只執(zhí)行一次,第二種每一秒執(zhí)行一次。
第三種測試
public?class?ScheduledThreadPoolDemo?{
????static?ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
????public?static?void?main(String[] args) throws InterruptedException {
????????service.scheduleWithFixedDelay(() -> {
????????????System.out.println("定時線程執(zhí)行3:"?+ LocalDateTime.now());
????????????try?{
????????????????Thread.sleep(1000);
????????????} catch?(InterruptedException e) {
????????????????e.printStackTrace();
????????????}
????????}, 1, 1, TimeUnit.SECONDS);
????}
}執(zhí)行結(jié)果
定時線程執(zhí)行3:2020-06-24T11:32:30.871
定時線程執(zhí)行3:2020-06-24T11:32:32.872
定時線程執(zhí)行3:2020-06-24T11:32:34.873
定時線程執(zhí)行3:2020-06-24T11:32:36.873
定時線程執(zhí)行3:2020-06-24T11:32:38.874
定時線程執(zhí)行3:2020-06-24T11:32:40.874每 2 秒執(zhí)行一次,和第二種(scheduleAtFixedRate)不一樣的,第二種(scheduleAtFixedRate)是不論線程執(zhí)行是否完成,都是每 1 秒執(zhí)行一次,而 scheduleWithFixedDelay 必須等上一次任務(wù)執(zhí)行完成。
4.SingleThreadExecutor
構(gòu)造函數(shù)如下:
return?new?FinalizableDelegatedExecutorService
????????????(new?ThreadPoolExecutor(1, 1,
????????????????????????????????????0L, TimeUnit.MILLISECONDS,
????????????????????????????????????new?LinkedBlockingQueue())); 它會使用唯一的線程去執(zhí)行任務(wù),原理和 FixedThreadPool 是一樣的,只不過這里線程只有一個,如果線程在執(zhí)行任務(wù)的過程中發(fā)生異常,線程池也會重新創(chuàng)建一個線程來執(zhí)行后續(xù)的任務(wù)。這種線程池由于只有一個線程,所以非常適合用于所有任務(wù)都需要按被提交的順序依次執(zhí)行的場景,而前幾種線程池不一定能夠保障任務(wù)的執(zhí)行順序等于被提交的順序,因為它們是多線程并行執(zhí)行的。
5.SingleThreadScheduledExecutor
相關(guān)創(chuàng)建的源碼
public?static?ScheduledExecutorService newSingleThreadScheduledExecutor()?{
????????return?new?DelegatedScheduledExecutorService
????????????(new?ScheduledThreadPoolExecutor(1));
????}它實際和第三種 ScheduledThreadPool 線程池非常相似,它只是 ScheduledThreadPool 的一個特例,內(nèi)部只有一個線程。
6.五種線程池對比

7.ForkJoinPool
ForkJoinPool 線程池在 JDK 8 加入,主要用法和之前的線程池是相同的,也是把任務(wù)交給線程池去執(zhí)行,線程池中也有任務(wù)隊列來存放任務(wù),和之前的五種線程池不同的是,它非常適合執(zhí)行可以分解子任務(wù)的任務(wù),比如樹的遍歷,歸并排序,或者其他一些遞歸場景。

如圖所示,我們有一個 Task,這個 Task 可以產(chǎn)生三個子任務(wù),三個子任務(wù)并行執(zhí)行完畢后將結(jié)果匯總給 Result,比如說主任務(wù)需要執(zhí)行非常繁重的計算任務(wù),我們就可以把計算拆分成三個部分,這三個部分是互不影響相互獨立的,這樣就可以利用 CPU 的多核優(yōu)勢,并行計算,然后將結(jié)果進行匯總。這里面主要涉及兩個步驟,第一步是拆分也就是 Fork,第二步是匯總也就是 Join,到這里我們應(yīng)該已經(jīng)了解到 ForkJoinPool 線程池名字的由來了。
7.1 斐波那契數(shù)列
這個數(shù)列的特點就是后一項的結(jié)果等于前兩項的和,第 0 項是 0,第 1 項是 1,那么第 2 項就是 0+1=1,以此類推。
0、1、1、2、3、5、8、13…
遞推公式如下:
f(n) = f(n-1)+f(n-2);
終止條件:n<=1
根據(jù)遞推公式的偽代碼如下:
protected?Integer f(int?n)?{
????????????if?(n <= 1) {
????????????????return?n;
????????????}
????????????int?a = f(n - 1);
????????????int?b = f(n - 2);
????????????return?a + b;
????????}7.2 ForkJoinPool 代碼實現(xiàn)
public?class?ForkJoinPoolDemo?{
????public?static?void?main(String[] args)?throws?ExecutionException, InterruptedException {
????????ForkJoinPool forkJoinPool = new?ForkJoinPool();
????????for?(int?i = 0; i < 10; i++) {
????????????ForkJoinTask task = forkJoinPool.submit(new?Fibonacci(i));
????????????System.out.println(task.get());
????????}
????}
????static?class?Fibonacci?extends?RecursiveTask<Integer> {
????????int?n;
????????public?Fibonacci(int?n)?{
????????????this.n = n;
????????}
????????@Override
????????protected?Integer compute()?{
????????????if?(n <= 1) {
????????????????return?n;
????????????}
????????????Fibonacci fib1 = new?Fibonacci(n - 1);
????????????fib1.fork();
????????????Fibonacci fib2 = new?Fibonacci(n - 2);
????????????fib2.fork();
????????????return?fib1.join() + fib2.join();
????????}
????}
}執(zhí)行結(jié)果:
0
1
1
2
3
5
8
13
21
34對比歸并算法,fork 的過程就相當(dāng)于 拆分 的過程,join 的過程就相當(dāng)于 合并 的過程。
7.3 ForkJoinPool 中的任務(wù)隊列
前面五種線程池,線程使用都是的同一個任務(wù)隊列(workQueue),但是 ForkJoinPool 線程池中每個線程都有自己獨立的任務(wù)隊列。
ForkJoinPool 線程池內(nèi)部除了有一個共用的任務(wù)隊列之外,每個線程還有一個對應(yīng)的雙端隊列 deque,這時一旦線程中的任務(wù)被 Fork 分裂了,分裂出來的子任務(wù)放入線程自己的 deque 里,而不是放入公共的任務(wù)隊列中。如果此時有三個子任務(wù)放入線程 t1 的 deque 隊列中,對于線程 t1 而言獲取任務(wù)的成本就降低了,可以直接在自己的任務(wù)隊列中獲取而不必去公共隊列中爭搶也不會發(fā)生阻塞( steal 情況除外),減少了線程間的競爭和切換,是非常高效的。
deque 雙端隊列
deque 是一種具有 隊列 和 棧 的性質(zhì)的數(shù)據(jù)結(jié)構(gòu)。

work-stealing 是什么?
假設(shè)此時線程有多個任務(wù),線程 t1 的任務(wù)特別繁重,分裂了數(shù)十個子任務(wù),但是 t0 此時卻無事可做,它自己的 deque 隊列為空,這時為了提高效率,t0 就會想辦法幫助 t1 執(zhí)行任務(wù),這就是 work-stealing。

雙端隊列 deque 中,線程 t1 獲取任務(wù)的邏輯是后進先出(棧的特點),也就是LIFO(Last In Frist Out),而線程 t0 在 steal 偷線程 t1 的 deque 中的任務(wù)的邏輯是先進先出(隊列的特點),也就是FIFO(Fast In Frist Out),如圖所示,圖中很好的描述了兩個線程使用雙端隊列分別獲取任務(wù)的情景。你可以看到,使用 work-stealing 算法和雙端隊列很好地平衡了各線程的負載。
出處:csdn.net/xiewenfeng520/article/details/106943304
覺得不錯點個“在看”哦!
