JUC學習之線程池工作原理

一、簡介

上一篇文章介紹了各種線程池的使用、優勢等,本篇我們將去了解線程池底層一點的相關知識。

二、線程池底層原理 

Executor爲我們提供了功能各異的線程池,其實其內部很多都是由ThreadPoolExecutor實現的,我們詳細瞭解下ThreadPoolExecutor實現原理不但對我們使用理解Executor提供的線程池大有幫助,也讓我們能根據實際情況自定義特定的線程池。

我們先看看幾種常用的線程池的方法,可以看到,基本上全是通過ThreadPoolExecutor來進行構建的:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

接着我們來看一下ThreadPoolExecutor類的構造方法:

/**
 * 使用給定的初始參數和默認線程工廠以及被拒絕的執行處理程序創建一個新的ThreadPoolExecutor。使用executor工廠方法而不是這個通用構造函數可能更方便.
 */
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
     //調用本類其他構造方法
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

/**
 * 使用給定的初始參數創建一個新的ThreadPoolExecutor.
 */
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    //可見maximumPoolSize 參數值必須大於等於corePoolSize 
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

可以看到ThreadPoolExecutor的構造方法中有七個參數,下面對這七大參數做一下說明:

  • 【a】corePoolSize:線程池中的常駐核心線程的數量(總線程量可大於等於這個值)
  • 【b】maximumPoolSize:線程池中能夠容納同時執行的最大線程數,此值必須大於等於1(總線程量不可能超越這個數值)
  • 【c】keepAliveTime:多餘的空閒線程的存活時間。當前池中線程數量超過corePoolSize時,當空閒時間達到keepAliveTime時,多餘線程會被銷燬直到只剩下corePoolSize個線程爲止。
  • 【d】unit:keepAliveTime的時間單位
  • 【e】workQueue:任務隊列,被提交但尚未被執行的任務;例如: 提交了10個任務,但是線程只有5個,於是另外5個提交但沒開始執行的任務就存放到了workQueue工作隊列裏面,既然是隊列,我們知道,實現隊列的方式有很多種,比如ArrayBlockQueue、LinkedBlockQueue等等,選擇不同的隊列就會帶來不同的問題。

ArrayBlockQueue:存在一個任務過多超出隊列長度;

LinkedBlockQueue:接受過多的任務可能會佔用太多內存,造成內存崩潰;

  • 【f】threadFactory:表示生成線程池中工作線程的線程工廠,用於創建線程,一般使用默認的線程工廠即可。
  • 【g】handler:拒絕策略,表示當隊列滿了,並且工作線程大於等於線程池的最大線程數(maximumPoolSize)時如何來拒絕請求執行的runnable的策略。默認拒絕策略是AbortPolicy,拋出異常阻止程序。

三、自定義線程池

在實際項目中,不太建議直接使用Executors工具類的方式來創建線程池,推薦根據項目具體需求自己創建線程池,動態配置ThreadPoolExecutor所需要的參數。自定義線程池直接通過ThreadPoolExecutor一個實例對象即可,下面我們通過一個簡單的示例說明如何自定義線程池。

public class T14_ThreadPool {

    public static void main(String[] args) {
        ExecutorService executorService = getExecutorService();
        //提交十個任務
        for (int i = 0; i < 10; i++) {
            final int num = i;
            executorService.execute(() -> System.out.println(Thread.currentThread().getName() + "=======獲取線程======, i = " + num));
        }
        executorService.shutdown();
    }

    private static ExecutorService getExecutorService() {
        return new ThreadPoolExecutor(
                5,  //常駐核心線程的數量
                5, //線程池中能夠容納同時執行的最大線程數
                0L, //多餘的空閒線程的存活時間
                TimeUnit.MILLISECONDS, //時間單位:毫秒
                new LinkedBlockingQueue<>(5), //阻塞隊列
                Executors.defaultThreadFactory(), //默認線程池生產工廠
                new ThreadPoolExecutor.AbortPolicy() //拒絕策略
        );
    }
}

運行結果:

pool-1-thread-1=======獲取線程======, i = 0
pool-1-thread-1=======獲取線程======, i = 5
pool-1-thread-1=======獲取線程======, i = 6
pool-1-thread-2=======獲取線程======, i = 1
pool-1-thread-2=======獲取線程======, i = 8
pool-1-thread-2=======獲取線程======, i = 9
pool-1-thread-1=======獲取線程======, i = 7
pool-1-thread-4=======獲取線程======, i = 3
pool-1-thread-5=======獲取線程======, i = 4
pool-1-thread-3=======獲取線程======, i = 2

可見,提交了十個任務,並且十個任務都成功執行了。下面我們提交十一個任務試一下:

//提交十一個任務
for (int i = 0; i < 11; i++) {
    final int num = i;
    executorService.execute(() -> System.out.println(Thread.currentThread().getName() + "=======獲取線程======, i = " + num));
}

運行結果:

pool-1-thread-1=======獲取線程======, i = 0
pool-1-thread-2=======獲取線程======, i = 1
pool-1-thread-1=======獲取線程======, i = 5
pool-1-thread-2=======獲取線程======, i = 6
pool-1-thread-1=======獲取線程======, i = 7
pool-1-thread-1=======獲取線程======, i = 9
pool-1-thread-2=======獲取線程======, i = 8
pool-1-thread-3=======獲取線程======, i = 2
java.util.concurrent.RejectedExecutionException: Task juc03.T14_ThreadPool$$Lambda$1/1078694789@7699a589 rejected from java.util.concurrent.ThreadPoolExecutor@58372a00[Running, pool size = 5, active threads = 5, queued tasks = 5, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
	at juc03.T14_ThreadPool.main(T14_ThreadPool.java:12)
pool-1-thread-4=======獲取線程======, i = 3
pool-1-thread-5=======獲取線程======, i = 4

可見,當我們提交的任務數量大於(corePoolSize + maximumPoolSize)時,由於我們使用的AbortPolicy拒絕策略,當遇到不能處理的任務時,直接報錯,報錯如上圖所示。

通過new ThreadPoolExecutor()我們就可以根據具體的業務場景很靈活地創建線程池,但是實際項目中corePoolSize 參數一般跟我們電腦處理器的數量掛鉤,然後workQueue工作隊列也建議使用有界阻塞隊列,避免任務太多,創建太多的線程。handler參數的話就需要根據具體需求來定了,下面我們也會單獨對四種拒絕策略做一個介紹。

下面給出一個參考的線程池創建方式:

private static ExecutorService getExecutorService() {
    //常駐核心線程的數量 : 最大可用的處理器數量 * 2
    int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
    //創建有界阻塞隊列
    ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(256);
    //拒絕策略
    RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.DiscardPolicy();
    return new ThreadPoolExecutor(
            corePoolSize,  //常駐核心線程的數量
            corePoolSize, //線程池中能夠容納同時執行的最大線程數
            0L, //多餘的空閒線程的存活時間
            TimeUnit.MILLISECONDS, //時間單位:毫秒
            workQueue, //阻塞隊列
            Executors.defaultThreadFactory(), //默認線程池生產工廠
            rejectedExecutionHandler //拒絕策略
    );
}

四、線程池拒絕策略

ThreadPoolExecutor類中存在四個私有靜態內部類AbortPolicy、DiscardPolicy、DiscardOldestPolicy、CallerRunsPolicy,這四個類就是它提供的四種拒絕策略相關類.

線程池給我們提供了幾種常見的拒絕策略:

下面對各種拒絕策略做一個介紹:

拒絕策略

拒絕行爲

AbortPolicy

默認的拒絕策略,當提交的任務數量大於線程池中的最大數量時,會拋出RejectedExecutionException,阻止系統正常運行。

DiscardPolicy

該策略默默地丟棄無法處理的任務,不予任何處理也不拋出異常,如果允許任務丟失,這是最好的一種策略(什麼也不做,直接忽略).

DiscardOldestPolicy

拋棄隊列中等待最久的任務,然後把當前任務加入隊列中,嘗試再次提交當前任務(丟棄執行隊列中最老的任務,嘗試爲當前提交的任務騰出位置)

CallerRunsPolicy

“調用者運行”一種調節機制,該策略既不會拋棄任務,也不會拋出異常,而是將某些任務回退到調用者,從而降低新任務的流量(直接由提交任務者執行這個任務)。

線程池默認的拒絕行爲是AbortPolicy,也就是拋出RejectedExecutionHandler異常,該異常是非受檢異常,很容易忘記捕獲。如果不關心任務被拒絕的事件,可以將拒絕策略設置成DiscardPolicy,這樣多餘的任務會悄悄的被忽略。

下面通過示例瞭解各種拒絕策略的使用:

AbortPolicy默認的拒絕策略在前面的示例已經講解過,這裏就不重複講解了。

【a】CallerRunsPolicy

public class T14_ThreadPool {

    public static void main(String[] args) {
        ExecutorService executorService = getExecutorService();
        //提交十一個任務
        for (int i = 0; i < 11; i++) {
            final int num = i;
            executorService.execute(() -> System.out.println(Thread.currentThread().getName() + "=======獲取線程======, i = " + num));
        }
        executorService.shutdown();
    }

    private static ExecutorService getExecutorService() {
        return new ThreadPoolExecutor(
                5,  //常駐核心線程的數量
                5, //線程池中能夠容納同時執行的最大線程數
                0L, //多餘的空閒線程的存活時間
                TimeUnit.MILLISECONDS, //時間單位:毫秒
                new LinkedBlockingQueue<>(5), //阻塞隊列
                Executors.defaultThreadFactory(), //默認線程池生產工廠
                new ThreadPoolExecutor.CallerRunsPolicy() //拒絕策略
        );
    }
}

運行結果:

main=======獲取線程======, i = 10
pool-1-thread-1=======獲取線程======, i = 0
pool-1-thread-1=======獲取線程======, i = 5
pool-1-thread-2=======獲取線程======, i = 1
pool-1-thread-1=======獲取線程======, i = 6
pool-1-thread-3=======獲取線程======, i = 2
pool-1-thread-2=======獲取線程======, i = 7
pool-1-thread-3=======獲取線程======, i = 9
pool-1-thread-1=======獲取線程======, i = 8
pool-1-thread-4=======獲取線程======, i = 3
pool-1-thread-5=======獲取線程======, i = 4

可見,多餘的一個任務被任務的調用者main線程執行了。

【b】DiscardOldestPolicy

public class T14_ThreadPool {

    public static void main(String[] args) {
        ExecutorService executorService = getExecutorService();
        //提交十一個任務
        for (int i = 0; i < 11; i++) {
            final int num = i;
            executorService.execute(() -> System.out.println(Thread.currentThread().getName() + "=======獲取線程======, i = " + num));
        }
        executorService.shutdown();
    }

    private static ExecutorService getExecutorService() {
        return new ThreadPoolExecutor(
                5,  //常駐核心線程的數量
                5, //線程池中能夠容納同時執行的最大線程數
                0L, //多餘的空閒線程的存活時間
                TimeUnit.MILLISECONDS, //時間單位:毫秒
                new LinkedBlockingQueue<>(5), //阻塞隊列
                Executors.defaultThreadFactory(), //默認線程池生產工廠
                new ThreadPoolExecutor.DiscardOldestPolicy() //拒絕策略
        );
    }
}

運行結果:

pool-1-thread-1=======獲取線程======, i = 0
pool-1-thread-2=======獲取線程======, i = 1
pool-1-thread-3=======獲取線程======, i = 2
pool-1-thread-3=======獲取線程======, i = 8
pool-1-thread-3=======獲取線程======, i = 9
pool-1-thread-1=======獲取線程======, i = 7
pool-1-thread-2=======獲取線程======, i = 6
pool-1-thread-3=======獲取線程======, i = 10
pool-1-thread-4=======獲取線程======, i = 3
pool-1-thread-5=======獲取線程======, i = 4

可見,i = 5那一次是在隊列中等待最久的,使用這個策略拋棄等待最久的線程。

【c】DiscardPolicy

public class T14_ThreadPool {

    public static void main(String[] args) {
        ExecutorService executorService = getExecutorService();
        //提交十一個任務
        for (int i = 0; i < 11; i++) {
            final int num = i;
            executorService.execute(() -> System.out.println(Thread.currentThread().getName() + "=======獲取線程======, i = " + num));
        }
        executorService.shutdown();
    }

    private static ExecutorService getExecutorService() {
        return new ThreadPoolExecutor(
                5,  //常駐核心線程的數量
                5, //線程池中能夠容納同時執行的最大線程數
                0L, //多餘的空閒線程的存活時間
                TimeUnit.MILLISECONDS, //時間單位:毫秒
                new LinkedBlockingQueue<>(5), //阻塞隊列
                Executors.defaultThreadFactory(), //默認線程池生產工廠
                new ThreadPoolExecutor.DiscardPolicy() //拒絕策略
        );
    }
}

運行結果: 

pool-1-thread-1=======獲取線程======, i = 0
pool-1-thread-1=======獲取線程======, i = 5
pool-1-thread-1=======獲取線程======, i = 6
pool-1-thread-1=======獲取線程======, i = 7
pool-1-thread-1=======獲取線程======, i = 8
pool-1-thread-1=======獲取線程======, i = 9
pool-1-thread-2=======獲取線程======, i = 1
pool-1-thread-3=======獲取線程======, i = 2
pool-1-thread-4=======獲取線程======, i = 3
pool-1-thread-5=======獲取線程======, i = 4

 可見,i = 10那個任務已經被默默地拋棄掉了。

五、線程池執行流程

線程池的整體執行流程可以用下面的圖整體概括:

文字描述:

任務進來時,首先執行判斷,判斷常駐核心線程是否處於空閒狀態,如果不是,核心線程就先就執行任務,如果核心線程已滿,則判斷任務隊列是否有地方存放該任務,若果有,就將任務保存在任務隊列中,等待執行,如果滿了,在判斷最大可容納的線程數,如果沒有超出這個數量,就創建非核心線程執行任務,如果超出了,就調用handler實現拒絕策略。

六、如何配置線程池

根據不同場景配置不同的線程池參數,這是最理想的線程池創建方式,下面給出了幾種常用場景下的配置方法參考:

  • CPU密集型的任務

儘量使用較小的線程池,一般爲CPU核心數+1。 因爲CPU密集型任務使得CPU使用率很高,若開過多的線程數,會造成CPU過度切換。

  • IO密集型的任務

可以使用稍大的線程池,一般爲2*CPU核心數。 IO密集型任務CPU使用率並不高,因此可以讓CPU在等待IO的時候有其他線程去處理別的任務,充分利用CPU時間。

  • 混合型的任務

可以將任務分成IO密集型和CPU密集型任務,然後分別用不同的線程池去處理。 只要分完之後兩個任務的執行時間相差不大,那麼就會比串行執行來的高效。

七、總結

本文總結了線程池的底層實現ThreadPoolExecutor類,並講解了如何自定義線程池,最後面還總結了幾種線程池針對未能正確處理的任務的拒絕策略。在實際項目中,推薦自己自定義線程池,雖然Executors給我們提供了很多方便的工具類,但是自定義線程池可以更加靈活地應對各種場景。任何東西都有兩面性,線程池固然有很多優點,但是使用線程池也會存在一定的風險:

用線程池構建的應用程序容易遭受任何其它多線程應用程序容易遭受的所有併發風險,諸如同步錯誤和死鎖,它還容易遭受特定於線程池的少數其它風險,諸如與池有關的死鎖、資源不足,併發錯誤,線程泄漏,請求過載等等。

參考資料:

https://www.jianshu.com/p/6d941f0ded66

https://blog.csdn.net/weixin_40271838/article/details/79998327

https://blog.csdn.net/codertnt/article/details/78971506

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章