重識 java 線程池

一. 什麼是線程池

線程池就是提前創建若干個線程, 如果有任務需要處理, 線程池裏的線程就會處理任務. 處理完之後線程並不會被銷燬, 而是等待下一個任務.

 

二. 爲什麼要使用線程池

在開發中, 幾乎所有需要異步或併發執行任務的程序都可以使用線程池. 合理使用線程池可以帶來以下 3 個好處

  1. 降低資源消耗. 通過重複利用已創建的線程降低線程創建和銷燬造成的消耗.
  2. 可以控制最大併發數. 避免大量線程之間因相互搶佔系統資源而導致的阻塞現象.
  3. 提高線程的可管理性. 線程是稀缺資源, 如果無限制的創建, 不僅會消耗系統資源, 還會降低系統的穩定性. 使用線程池可以進行統一分配, 調優和監控.
     

三. Executor

Java 5之後引入了一堆新的啓動, 調度和管理線程的API. Executor 框架便是 Java 5 中引入的, 其內部使用了線程池機制, 它在 java.util.cocurrent 包下. 通過該框架來控制線程的啓動, 執行和關閉, 可以簡化併發編程的操作. 因此, 在 Java 5 之後, 通過使用 Executor 框架來啓動線程比使用Threadstart方法更好.

  • Executor
    Executor 是一個接口, 它是 Executor 框架的基礎, 它將任務的提交與任務的執行分離開來. 內部定義了一個接收 Runnable 對象的方法 executor. 該方法接收一個 Runable 實例, 它用來執行一個任務, 任務即一個實現了 Runnable 接口的類.
     
  • ExecutorService
    ExecutorService 是一個比 Executor 使用更廣泛的子類接口, 其提供了生命週期管理的方法, 以及可跟蹤一個或多個異步任務執行狀況方法. 所以說 ExecutorService可以說是真正的線程池接口.
     
  • ScheduledExecutorService
    ScheduledExecutorService 也是一個接口, 繼承了 ExecutorService 接口. 內部定義多 4 個帶週期執行功能的方法.
     
  • ScheduledThreadPoolExecutor
    ScheduledThreadPoolExecutor 繼承了 ScheduledExecutorService 接口, 並繼承了 ThreadPoolExecutor 類. 可以在給定的延遲後運行命令, 或者定期執行命令. ScheduledExecutorServiceTimer 更靈活, 功能也比較強大.
     
  • AbstractExecutorService
    AbstractExecutorService 是一個抽象類, 實現了 ExecutorService 接口中的大部分方法.
     
  • ThreadPoolExecuto
    ThreadPoolExecutor 繼承自抽象類 AbstractExecutorService. 是線程池的核心實現類. 它的構造方法提供了一系列參數來配置線程池, 這些參數將會直接影響到線程池的功能特性.
     
提交任務
  1. Executorexecute() 方法用於提交不需要返回值的任務, 所以無法判斷任務是否被線程執行成功.

  2. ExecutorServicesubmit() 方法用於提交需要有返回值的任務. 線程池會返回一個 Future 類型的對象, 可以調用 isDone() 方法查詢 Future 是否已經完成. 當任務完成時, 它有一個結果. 可以調用 get() 方法來獲取該結果. 也可以不用 isDone() 進行檢查就直接調用 get() 獲取結果, 在這種情況下, get() 將阻塞當前線程, 直至結果準備就緒. 還可以取消任務的執行, Future 提供了 cancel() 方法用來取消執行 pending 中的任務.
     

關閉線程池
  1. ExecutorServiceshutdown() 或者 shutdownNow() 方法用來關閉線程池. 它們的原理是遍歷線程池中的工作線程, 然後逐個調用線程的 interrupt() 方法來中斷線程, 所以無法響應中斷的任務可能永遠無法停止. 但是他們存在一定的區別.

    • shutdownNow() 首先將線程池的狀態設置爲 STOP, 然後嘗試停止所有的正在執行或者暫停任務的線程, 並返回等待執行任務的列表.
    • shutdown() 只是將線程池的狀態設置成 SHUTDOWN 狀態. 然後中斷所有沒有正在執行任務的線程.
  2. 只要調用了這兩個方法中的任意一個, ExecutorService.isShutdown() 方法就會返回 true. 當所有的任務都已關閉後, 才表示線程池關閉成功, 這時調用 ExecutorService.isTerminated() 方法會返回 true. 至於應該調用哪一種方法來關閉線程池, 應該由提交到線程池的任務特性決定, 通常調用 ExecutorService.shutdown() 來平滑的關閉線程池, 如果任務不一定要執行完, 則可以使用 ExecutorService.shutdownNow()

 
瞭解瞭如何提交任務到線程池與如何關閉線程池後, 那麼接下來就來看一下 ThreadPoolExecutor 的構造函數中配置線程池的參數都有什麼意義吧.

 

四. ThreadPoolExecutor

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • int corePoolSize
    線程池的核心線程數. 當提交一個任務時, 如果線程池中的線程數量未達到核心線程的數量, 那麼會直接創建一個線程作爲核心線程來執行任務. 如果線程中的線程數量已經達到或者超過核心線程的數量, 那麼會將任務放入到阻塞隊列 workQueue 中排隊等待執行.

    如果執行了 ThreadPoolExecutorprestartAllCoreThreads() 方法, 線程池會提前創建並啓動所有的核心線程.

    默認情況下, 核心線程會在線程池中一直存活, 即使它們處於閒置狀態. 如果將 ThreadPoolExecutorallowCoreThreadTimeOut 屬性設置爲 true, 那麼閒置的核心線程在等待新任務到來時會有超時策略, 這個時間間隔由 keepAliveTime 參數指定. 當等待時間超過 keepAliveTime 所指定的時間後, 核心線程就會被終止.
     

  • int maximumPoolSize
    線程池所能容納的最大線程數, 如果當前阻塞隊列滿了, 且又繼續提交任務, 則會創建新的線程直接執行新添加的任務, 前提是當前線程池中線程數小於 maximumPoolSize.
     

  • long keepAliveTime
    非核心線程的閒置時的超時時長. 超過這個時長, 非核心線程就會被回收. 當 ThreadPoolExecutorallowCoreThreadTimeOut 屬性設置爲 true 的時候, keepAliveTime 同樣會作用於核心線程.
     

  • TimeUnit unit
    用於指定 keepAliveTime 參數的時間單位, 是一個枚舉, 常用的有 TimeUnit.MILLISECONDS(毫秒), TimeUnit.SECONDS(秒) 以及 TimeUnit.MINUTES(分鐘) 等 .
     

  • BlockingQueue<Runnable> workQueue
    線程池中的阻塞任務隊列. 通過線程池的 execute 方法提交的 Runnable 對象會存儲在這個對列中.

    上一章的阻塞隊列中有說過阻塞隊列分爲有界與無界的, 在這裏我們儘量使用有界隊列, 如果使用的是無界隊列可能會耗盡系統資源, 及時使用有界隊列, 也要儘量控制隊列的大小在一個合適的範圍.
     

  • ThreadFactory threadFactory
    線程工廠(可缺省), 通過自定義的線程工廠可以給每個新建的線程設置一個具有識別度的線程名.
    默認的線程命名規則爲 pool-數字-thread-數字
     

  • RejectedExecutionHandler handler
    線程池拒絕策略(可缺省), 當阻塞隊列滿了, 並且當前線程池內的線程數量大於 maximumPoolSize 最大線程數量, 如果繼續提交任務, 就必須採取一種策略處理該任務. 線程池默認提供了 4 種策略.
    AbortPolicy: 直接拋出異常. 線程池中默認的拒絕策略.
    DiscardOldestPolicy : 直接丟棄阻塞隊列中最老的任務, 也就是最前面的任務, 並執行當前任務.
    CallerRunsPolicy: 提交任務所在的線程來執行這個要提交的任務.
    DiscardPolicy: 直接丟棄最新的任務, 也就是最後面的.
    也可以根據場景來實現 RejectedExecutionHandler 接口, 自定義拒絕策略, 比如記錄日誌或者持久化存儲被拒絕的任務.
     

根據以上參數理解, 可以總結出當有新任務需要處理的時候 ThreadPoolExecuto 執行任務的流程大致如下

  1. 先看線程池中的線程數量是否小於核心線程 corePoolSize 的數量
    • 小於核心線程數, 那麼會直接啓動一個核心線程來執行任務.
  2. 大於或等於核心線程數, 接着看任務隊列 workQueue是否滿了.
    • 任務隊列未滿, 那麼就直接插入到任務隊列中等待執行
  3. 任務隊列 workQueue 滿了, 最後看線程池中的線程數量是否小於線程池最大線程數 maximumPoolSize.
    • 小於線程池最大線程數, 立刻啓動一個非核心線程來執行任務.
  4. 大於或等於線程池最大線程數, 則執行拒絕策略.
     

下面以一個示例來使用一下 ThreadPoolExecuto

class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "正在執行。。。");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class TestType5 {
    public static void main(String[] args) throws Exception {
        //定義一個容量爲 2 的有界阻塞隊列
        BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(2);
        //創建一個線程池, 核心線程 3 個, 最大線程數 5 個, 60 秒鐘超時
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 5, 60, TimeUnit.SECONDS, queue);
        Runnable r1= new MyTask();
        Runnable r2= new MyTask();
        Runnable r3= new MyTask();
        Runnable r4= new MyTask();
        Runnable r5= new MyTask();
        Runnable r6= new MyTask();
        Runnable r7= new MyTask();
        Runnable r8= new MyTask();

        threadPoolExecutor.execute(r1);
        threadPoolExecutor.execute(r2);
        threadPoolExecutor.execute(r3);
        threadPoolExecutor.execute(r4);
        threadPoolExecutor.execute(r5);
        threadPoolExecutor.execute(r6);
        threadPoolExecutor.execute(r7);
        //threadPoolExecutor.execute(r8);
        threadPoolExecutor.shutdown();
    }
}

輸出結果爲:

pool-1-thread-1正在執行。。。
pool-1-thread-2正在執行。。。
pool-1-thread-3正在執行。。。
pool-1-thread-4正在執行。。。
pool-1-thread-5正在執行。。。
pool-1-thread-1正在執行。。。
pool-1-thread-5正在執行。。。

可是, 如果把 r8 也執行了, 輸出結果就會變成下面這樣

pool-1-thread-1正在執行。。。
pool-1-thread-3正在執行。。。
pool-1-thread-4正在執行。。。
pool-1-thread-2正在執行。。。
pool-1-thread-5正在執行。。。

Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task org.study.MyTask@6f94fa3e rejected from java.util.concurrent.ThreadPoolExecutor@5e481248
[Running, pool size = 5, active threads = 5, queued tasks = 2, 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 org.study.TestType5.main(TestType5.java:42)

pool-1-thread-2正在執行。。。
pool-1-thread-3正在執行。。。

可以看到執行了拒絕策略, 拋出了默認的異常. 爲什麼呢? 我們來屢屢. 一共創建了 8 個Runnable, 分別是 r1 r2 r3 ... r8 .
創建的線程池核心線程爲 3 個, 最大線程總數爲 5 個. 阻塞隊列長度爲 2.

  1. r1, r2, r3 這三個分別在開始執行時發現當前線程池內線程總數(0)小於核心線程總數(3), 那麼就會啓動三個核心線程來執行r1 - r3.
  2. 執行 r4 的時候, 發現線程池內的線程總數已經等於了核心線程數, 那麼看到任務隊列未滿, 就放入到阻塞隊列中.
  3. r5的執行與 r4 流程相同., r5 執行完後, 此時線程池中線程數量爲 3, 阻塞隊列也滿了.
  4. 執行 r6 的時候發現當前線程池內的線程總數等於核心線程, 並且阻塞隊列也滿了, 最後拿當前線程池中的線程總數(3)與我們設置的線程池最大線程數(5)做對比, 發現小於我們設置的最大線程數, 那麼就新啓動一個線程來執行 r6.
  5. r7 的執行與 r6 相同, 也是新啓動一個線程來執行 r7. 這時候, 核心線程總數爲 3, 阻塞隊列滿了, 線程池中的線程總數爲 5 個.
  6. 當要執行 r8 的時候, 發現核心線程數滿了, 阻塞隊列也滿了, 同時線程池中的線程總數也達標了, 那麼就會執行拒絕策略, 拋出默認的異常.
  7. 目前正在執行的任務有 r1, r2, r3, r6, r7, 阻塞隊列中的任務是 r5, r6. 當 r1, r2, r3, r6, r7 中任何一個任務執行完了, 就會從阻塞隊列中取出任務來執行, 所以最後又打印了兩個 log. 執行的就是阻塞隊列中的 r5r6.

看到這裏的同學是不是有點蒙圈了, 通過 ThreadPoolExecutor 配置的線程池, 那些參數要如何填寫呢? 核心線程要分配幾個呢? 怎麼配置線程池算是合理呢? 其實想要合理的配置線程池, 需要先分析任務的特性. 而任務的特性可以從以下幾個角度來分析

  • 任務的性質: CPU 密集型任務, IO 密集型任務和混合型任務.
  • 任務的優先級: 高, 中, 低.
  • 任務的執行時間: 長, 中, 短
  • 任務的依賴性: 是否依賴其他系統資源, 如數據庫連接.

CPU 密集型任務的特點是要進行大量的計算, 消耗 CPU 資源, 比如計算圓周率, 對視頻進行高清解碼等, 全靠 CPU 的運算能力.
IO 密集型是指設計到網絡, 磁盤 IO 的任務 都是 IO 密集型任務, 這類任務特點是 CPU 消耗很少, 任務的大部分時間都在等待 IO 操作完成 (因爲 IO 的速度遠遠低於 CPU 和內存的速度).

性質不能同的任務可以用不同規模的線程池分開處理.

  • CPU 密集型的任務應配置儘可能小的線程, 因爲CPU密集型任務使得CPU使用率很高, 若開過多的線程數, 只能增加上下文切換的次數, 因此會帶來額外的開銷. 如配置 CPU +1 個線程的線程池.
  • IO密集型任務CPU使用率並不高, 因此可以讓 CPU 在等待 IO 的時候去處理別的任務, 充分利用 CPU, 則應該配置儘可能多的線程, 如 2*CPU.
  • 混合型任務, 如果可以拆分, 將其拆分成一個 CPU 密集型任務和一個 IO 密集型任務, 只要這兩個任務執行的時間相差不是太大, 那麼分解後執行的吞吐量將高於串行執行的吞吐量. 如果這兩個任務執行的時間相差太大, 則沒有必要進行分解.
  • 優先級不同的任務可以使用優先級隊列 PriorityBlockingQueue 來處理, 它可以讓優先級高的任務優先執行.
  • 執行時間不同的任務可以交給不同規模的線程池來處理, 或者可以使用優先級隊列, 讓執行時間短的優先執行.
  • 建議使用有界隊列, 有界隊列能增加系統的穩定性和預警能力, 可以根據需要設置. 如果設置爲無界隊列, 那麼線程池的隊列內的任務就會越來越多, 有可能會撐滿內存, 導致整個系統不可用等各種異常.

Ps: 通過 Runtime.getRuntime().availableProcessors() 方法獲得當前設備的 CPU 個數
Ps: CPU+1 是爲了防止頁缺失.也叫硬中斷. 當計算密集型的線程偶爾由於缺失故障或者其他原因而暫停時, 這個額外的線程也能確保 CPU 的時鐘週期不會被浪費.


這裏瞭解了通過 ThreadPoolExecutor 實現線程池方式, 而在 Java 中, 使用 Executors 類創建線程池也是一種常用的創建線程池的方式. 下面再來了解一下 Executors類.
 

五. Executors

Executors 類, 提供了一系列工廠方法用於創建線程池,返回的線程池都實現了 ExecutorService 接口.
通過 Executors 類創建的 4 類線程池都是直接或間接的通過配置ThreadPoolExecutor 來實現自己的功能特性, 下面分別來了解一下通過 Executors 創建的這 4 類線程池.
 

1. newCachedThreadPool 緩存型線程池
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
  • 通過 newCachedThreadPool 創建的是一種線程數量不定的線程池, 它只有又非核心線程, 並且設置了最大線程池數爲 Integer.MAX_VALUE. 由於 Integer.MAX_VALUE 是一個很大的數, 實際上就相當於最大線程數可以任意大.

  • 當線程池中的線程都處於活動狀態時, 線程池會創建新的線程來處理新任務, 否則就會利用空閒的線程來處理新任務.

  • 線程池內的空閒線程超時時間爲 60 秒, 超過 60 秒閒置的線程就會被回收.

  • 線程池內的任務隊列其實相當於一個空集合, 這將導致任何任務都會被立即執行, 因爲在這種場景下 SynchronousQueue 阻塞隊列是無法插入任務的. 詳情見上一章的
    「java 中的阻塞隊列」

newCachedThreadPool 創建出的線程池特性來看, 這類線程池比較適合執行大量的耗時較少的任務, 當整個線程池都處於閒置狀態時, 線程池中的線程都會因爲超時而被停止, 這時候線程池內部是沒有任何線程的, 幾乎不佔用任何系統資源.
 

2. newFixedThreadPool 全核心型線程池
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
  • 通過 newFixedThreadPool 創建的是一種線程數量固定的線程池.

  • 當線程池內的線程處於空閒狀態時, 並不會被回收, 除非線程池被關閉了.

  • 當所有的線程都處於活動狀態時, 新任務都會處於等待狀態, 直到有線程空閒出來.

  • 由於只有核心線程並且核心線程不會被回收, 這意味着它能夠更加快速的響應外接的請求.

這類型的線程池多數針對一些很穩定很固定的正規併發線程, 多用於服務器.
 

3. newScheduledThreadPool 調度型線程池
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    // 上面的 Executor 框架 UML 圖 中 ScheduledThreadPoolExecutor  繼承了 ThreadPoolExecutor 類.
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());
    }
  • 核心線程數固定, 而非核心線程池數沒有限制.

  • 當非核心線程閒置時會被立即回收.

  • 這個池子裏的線程可以按 schedule 依次 delay 執行, 或週期執行.

這類型的線程池主要用於執行定時任務和具有固定週期的重複任務.

 

4. newSingleThreadExecutor 單例型線程池
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }
  • 線程池內部只有一個核心線程, 它確保所有的任務都在同一個線程中按順序執行.

  • newSingleThreadExecutor 的意義在於統一所有的外界任務到一個線程中, 這使的這些任務之間不需要處理線程同步的問題.
     


下面只挑出兩個具有代表性的來看一下是如何使用的, newCachedThreadPoolnewScheduledThreadPool

帶有返回值的任務提交與 newCachedThreadPool 的使用.
class MyTask implements Callable<String>{

    private int id;

    public MyTask(int id) {
        this.id = id;
    }

    @Override
    public String call() throws Exception {
        System.out.println("id:"+ id + " - threadName:"+Thread.currentThread().getName() + "調用 call 方法");
        //這裏返回的結果, 會被 Future 的 get 方法得到.
        return "任務返回結果爲:" +  id +" - "+ Thread.currentThread().getName();
    }
}

public class TestType5 {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newCachedThreadPool();
        //存儲返回的結果
        ArrayList<Future<String>> list = new ArrayList<>();

        //提交 10 個任務, 並將返回的 future 存儲
        for (int i = 0; i < 10; i++) {
            Future<String> future = pool.submit(new MyTask(i));
            list.add(future);
        }

        for (Future<String> fs : list) {
            //Future 返回如果沒有完成, 則一直循環,
            while (!fs.isDone());
            System.out.println(fs.get());
            pool.shutdown();
        }

    }
}

輸出結果

id:0 - threadName:pool-1-thread-1調用 call 方法
id:3 - threadName:pool-1-thread-4調用 call 方法
id:2 - threadName:pool-1-thread-3調用 call 方法
id:1 - threadName:pool-1-thread-2調用 call 方法
id:5 - threadName:pool-1-thread-6調用 call 方法
id:4 - threadName:pool-1-thread-5調用 call 方法
id:6 - threadName:pool-1-thread-7調用 call 方法
id:7 - threadName:pool-1-thread-6調用 call 方法
id:9 - threadName:pool-1-thread-5調用 call 方法
id:8 - threadName:pool-1-thread-7調用 call 方法
任務返回結果爲:0 - pool-1-thread-1
任務返回結果爲:1 - pool-1-thread-2
任務返回結果爲:2 - pool-1-thread-3
任務返回結果爲:3 - pool-1-thread-4
任務返回結果爲:4 - pool-1-thread-5
任務返回結果爲:5 - pool-1-thread-6
任務返回結果爲:6 - pool-1-thread-7
任務返回結果爲:7 - pool-1-thread-6
任務返回結果爲:8 - pool-1-thread-7
任務返回結果爲:9 - pool-1-thread-5
newScheduledThreadPool 調度型線程池的使用
class ThreadPoolUtil implements Runnable{

    private Integer index;

    public ThreadPoolUtil(Integer index) {
        this.index = index;
    }

    @Override
    public void run() {
        try {
            System.out.println(index+"開始處理線程!");
            Thread.sleep(5000);
            System.out.println("線程標識是:"+this.toString());
            System.out.println(index+"處理結束!");
        }
        catch(InterruptedException e) {
            e.printStackTrace();
        }
    }

}

public class TestType5 {
    public static void main(String[] args) throws Exception {
        //核心線程爲 2. 一次執行 2 個任務.剩下的放入到隊列
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
        for (int i = 0; i < 4; i++) {
            //延遲 2 秒執行
            pool.schedule(new ThreadPoolUtil(i), 2 , TimeUnit.SECONDS);
        }
        pool.shutdown();
    }
}

輸出結果

0開始處理線程!
1開始處理線程!
線程標識是:org.study.ThreadPoolUtil@30f68028
線程標識是:org.study.ThreadPoolUtil@37c3a03e
0處理結束!
1處理結束!
2開始處理線程!
3開始處理線程!
線程標識是:org.study.ThreadPoolUtil@34c912ee
線程標識是:org.study.ThreadPoolUtil@390458b6
2處理結束!
3處理結束!

 
線程池到這裏就結束了, 我們在 Android 中有時也會需要用到線程池. 所以瞭解類之間的關係以及線程池的優化也是必不可少的.

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