線程池全面解析,一篇就夠了!

爲何使用線程池

通常狀況有下面三個緣由java

  • 建立/銷燬線程伴隨着系統開銷,過於頻繁的建立/銷燬線程,會很大程度上影響處理效率,應用可以更加充分合理地協調利用CPU、內存、網絡、I/O等系統資源;

爲何會影響處理效率呢?
答:線程的建立須要開闢虛擬機棧、本地方法棧、程序計數器等線程私有的內存空間; 在線程銷燬時須要回收這些系統資源.頻繁地建立和銷燬線程會浪費大量的系統資源,增長併發編程風險.android

  • 線程併發數量過多,搶佔系統資源從而致使阻塞,利用線程池管理並複用線程,控制最大併發數;
  • 使用線程池能夠對線程進行一些簡單的管理,實現任務線程隊列緩存策略和拒絕機制,實現某些與時間相關的功能,例如定時執行、週期執行等,另外還能夠隔離線程環境;

線程池ThreadPoolExecutor

Android中線程池來自於Java,那麼研究Android線程池其實也能夠說是研究Java中的線程池git

在Java中,線程池的概念是Executor這個接口,具體實現爲ThreadPoolExecutor類,學習Java中的線程池,就能夠直接學習他了面試

對線程池的配置,就是對ThreadPoolExecutor構造函數的參數的配置,既然這些參數這麼重要,就來看看構造函數的各個參數吧數據庫

ThreadPoolExecutor的重要參數

  • corePoolSize:核心線程數核心線程會一直存活,即便沒有任務須要執行 當線程數小於核心線程數時,即便有線程空閒,線程池也會優先建立新線程處理 設置allowCoreThreadTimeout=true(默認false)時,核心線程會超時關閉
  • workQueue:任務隊列(阻塞隊列)當核心線程數達到最大時,新任務會放在隊列中排隊等待執行,該線程池中的任務隊列:維護着等待執行的Runnable對象

當全部的核心線程都在幹活時,新添加的任務會被添加到這個隊列中等待處理,若是隊列滿了,則新建非核心線程執行任務編程

經常使用的workQueue類型:緩存

  1. SynchronousQueue:這個隊列接收到任務的時候,會直接提交給線程處理,而不保留它,若是全部線程都在工做怎麼辦?那就新建一個線程來處理這個任務!因此爲了保證不出現<線程數達到了maximumPoolSize而不能新建線程>的錯誤,使用這個類型隊列的時候,maximumPoolSize通常指定成Integer.MAX_VALUE,即無限大安全

  2. LinkedBlockingQueue:這個隊列接收到任務的時候,若是當前線程數小於核心線程數,則新建線程(核心線程)處理任務;若是當前線程數等於核心線程數,則進入隊列等待。因爲這個隊列沒有最大值限制,即全部超過核心線程數的任務都將被添加到隊列中,這也就致使了maximumPoolSize的設定失效,由於總線程數永遠不會超過corePoolSizebash

  3. ArrayBlockingQueue:能夠限定隊列的長度,接收到任務的時候,若是沒有達到corePoolSize的值,則新建線程(核心線程)執行任務,若是達到了,則入隊等候,若是隊列已滿,則新建線程(非核心線程)執行任務,又若是總線程數到了maximumPoolSize,而且隊列也滿了,則發生錯誤網絡

  4. DelayQueue:隊列內元素必須實現Delayed接口,這就意味着你傳進去的任務必須先實現Delayed接口。這個隊列接收到任務時,首先先入隊,只有達到了指定的延時時間,纔會執行任務

  • maxPoolSize:最大線程數

    • 當線程數>=corePoolSize,且任務隊列已滿時。線程池會建立新線程來處理任務
    • 當線程數=maxPoolSize,且任務隊列已滿時,線程池會拒絕處理任務而拋出異常
  • keepAliveTime:線程空閒時間 當線程空閒時間達到keepAliveTime時,線程會退出,直到線程數量=corePoolSize 若是allowCoreThreadTimeout=true,則會直到線程數量=0 方法allowCoreThreadTimeout()能夠設置容許核心線程超時退出

  • TimeUnit :keepAliveTime的單位,TimeUnit是一個枚舉類型,其包括:

    • NANOSECONDS : 1微毫秒 = 1微秒 / 1000
    • MICROSECONDS : 1微秒 = 1毫秒 / 1000
    • MILLISECONDS : 1毫秒 = 1秒 /1000
    • SECONDS : 秒
    • MINUTES : 分
    • HOURS : 小時
    • DAYS : 天
  • ThreadFactory 建立線程的方式,是一個接口,new他的時候須要實現他的Thread

  • rejectedExecutionHandler:任務拒絕處理器

    • 兩種狀況會拒絕處理任務:
      • 當線程數已經達到maxPoolSize,切隊列已滿,會拒絕新任務
      • 當線程池被調用shutdown()後,會等待線程池裏的任務執行完畢,再shutdown。若是在調用shutdown()和線程池真正shutdown之間提交任務,會拒絕新任務 線程池會調用rejectedExecutionHandler來處理這個任務。若是沒有設置默認是AbortPolicy,會拋出異常

ThreadPoolExecutor類有幾個內部實現類來處理這類狀況:

  • AbortPolicy 丟棄任務,拋運行時異常
  • CallerRunsPolicy 執行任務
  • DiscardPolicy 忽視,什麼都不會發生
  • DiscardOldestPolicy 從隊列中踢出最早進入隊列(最後一個執行)的任務
  • 實現RejectedExecutionHandler接口,可自定義處理器

ThreadPoolExecutor執行順序

線程池按如下行爲執行任務

面試時可能常常會被問到的一個問題

  • 當線程數小於核心線程數時,建立核心線程執行任務。
  • 當線程數大於等於核心線程數,且任務隊列未滿時,將任務放入任務隊列。
  • 當線程數大於等於核心線程數,且任務隊列已滿
    • 若線程數小於最大線程數,建立線程執行任務
    • 若線程數等於最大線程數,拋出異常,拒絕任務,異常策略是上面介紹的那些類型

線程池中任務執行

通常狀況下有兩種方式能夠向線程池中提交任務

  • execute 調用execute()方法提交任務到線程池中進行執行,這個方法是沒有返回值的,比較經常使用,是ThreadPoolExecutor本身實現的方法;這種方式沒法判斷任務被線程池執行的狀況,好比是否成功;

  • submit 調用submit方法會有返回,完整的方法描述是:public Future<?> submit(Runnable task),其中泛型T是咱們回調的結果類型,若是咱們但願監放任務執行的結果,可使用submit方法提交任務,這個方法實際上是ThreadPoolExecutor的父類AbstractExecutorService的方法;
    返回的future可經過get()獲取返回值,get()會阻塞當前線程直到任務完成,而使用get(long timeout,TimeUnit unit)方法則會阻塞當前線程一段時間後當即返回,這時候可能任務沒有執行完.

默認的經常使用線程池

通常咱們可能不須要本身去根據實際須要進行參數設定對應的ThreadPoolExecutor來實現線程池,大部分狀況下,咱們使用java併發包中提供的Executors的類調用它的靜態方法獲取須要的線程池類型

  • newFixedThreadPool 建立固定線程數的線程池由於最大線程數和核心線程數相等,而且是無界隊列,可控制線程最大併發數(同時執行的線程數),超出的線程會在隊列中等待
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    
複製代碼
  • newSingleThreadExecutor 單任務隊列的線程池,最大線程數和核心線程數都是1,無界隊列,全部的任務都按照順序進行執行;
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
複製代碼
  • ScheduledThreadPool 支持定時週期性執行任務的線程池
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
複製代碼
  • CachedThreadPool 線程數無限制 有空閒線程則複用空閒線程,若無空閒線程則新建線程 必定程序減小頻繁建立/銷燬線程,減小系統開銷
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
複製代碼

線程池的關閉

可經過調用線程池的shutdown或shutdownNow方法來關閉線程池.

它們的原理是遍歷線程池中的工做線程,而後逐個調用線程的interrupt方法來中斷線程,因此沒法響應中斷的任務可能永遠沒法終止.

可是它們存在必定的區別

  • shutdownNow首先將線程池的狀態設置成STOP,而後嘗試中止全部的正在執行或暫停任務的線程,並返回等待執行任務的列表
  • shutdown只是將線程池的狀態設置成SHUTDOWN狀態,而後中斷全部沒有正在執行任務的線程.

只要調用了這兩個關閉方法中的任意一個,isShutdown方法就會返回true.

當全部的任務都已關閉後,才表示線程池關閉成功,這時調用isTerminaed方法會返回true.

至於應該調用哪種方法,應該由提交到線程池的任務的特性決定,一般調用shutdown方法來關閉線程池,若任務不必定要執行完,則能夠調用shutdownNow方法.

線程池的狀態

  • running狀態:當線程池建立後,初始爲 running 狀態
  • shutdown狀態:調用 shutdown 方法後,處在shutdown 狀態,此時再也不接受新的任務,等待已有的任務執行完畢
  • stop狀態:調用 shutdownnow 方法後,進入 stop 狀態,再也不接受新的任務,而且會嘗試終止正在執行的任務。
  • terminated狀態:當處於 shotdown 或 stop 狀態,而且全部工做線程已經銷燬,任務緩存隊列已清空,線程池被設爲 terminated 狀態。

線程池的配置選擇

要想合理地配置線程池,就必須首先分析任務特性,可從如下幾個角度來分析

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

針對不一樣性質的任務

使用不一樣規模的線程池進行處理

  • CPU密集型任務 應配置儘量小的線程,配置 N(CPU)+1或者 N(CPU) * 2
  • I/O密集型任務 業務讀取較多,線程並非一直在執行任務,則應配置儘量多的線程 N(CPU)/1 - 阻塞係數(0.8~0.9)
  • 混合型的任務 若是能夠拆分,將其拆分紅一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐量將高於串行執行的吞吐量.若是這兩個任務執行時間相差太大,則不必進行分解.

經過Runtime.getRuntime().availableProcessors()方法得到當前設備的CPU個數

針對優先級不一樣的任務

優先級不一樣的任務可使用PriorityBlockingQueue處理.它可讓優先級高 的任務先執行. 若是一直有優先級高的任務提交到隊列裏,那麼優先級低的任務可能永遠不能執行,這一點須要特別注意

針對執行時間不一樣的任務

執行時間不一樣的任務能夠交給不一樣規模的線程池來處理,或者可使用優先級隊列,讓執行時間短的任務先執行.

建議使用有界隊列 有界隊列能增長系統的穩定性和預警能力,若是咱們設置成無界隊列,那麼線程池的隊列就會愈來愈多,有可能會撐滿內存,致使整個系統不可用,而不僅是後臺任務出現問題.固然這些對於java後臺的開發比較關鍵,對於android的開發相對比較少一些;

線程池的監控

若是在系統中大量使用線程池,則有必要對線程池進行監控,方便在出現問題時,能夠根據線程池的使用情況快速定位問題.可經過線程池提供的參數進行監控,在監控線程池的時候可使用如下屬性:

taskCount:線程池須要執行的任務數量
completedTaskCount:線程池在運行過程當中已完成的任務數量,小於或等於taskCount。
largestPoolSize:線程池裏曾經建立過的最大線程數量.經過這個數據能夠知道線程池是否曾經滿過.如該數值等於線程池的最大大小,則表示線程池曾經滿過.
getPoolSize:線程池的線程數量.若是線程池不銷燬的話,線程池裏的線程不會自動銷燬,因此這個大小隻增不減.
getActiveCount:獲取活動的線程數.
複製代碼

經過擴展線程池進行監控.能夠經過繼承線程池來自定義線程池,重寫線程池的 beforeExecute、afterExecute和terminated方法,也能夠在任務執行前、執行後和線程池關閉前執行一些代碼來進行監控.例如,監控任務的平均執行時間、最大執行時間和最小執行時間等.

往期文章推薦
那些年你曾經工做過的奇葩公司能有多奇葩!
Kotlin擴展和對應的java代碼解析
git操做高級命令
瞭解一下計算機網絡層吧
計算機網絡基礎
帶你深刻了解運輸層
看看網絡安全的一些知識吧!
是時候瞭解一下應用層的知識了一塊兒來吧!
如何使用命令行卸載手機預知APP

長按二維碼關注公衆號,接收新的消息推送,值得期待喲!感謝您的支持!

技術乾貨店
相關文章
相關標籤/搜索