咱們在工做中或多或少都使用過線程池,可是爲何要使用線程池呢?從他的名字中咱們就應該知道,線程池使用了一種池化技術,和不少其餘池化技術同樣,都是爲了更高效的利用資源,例如連接池,內存池等等。數據庫
數據庫連接是一種很昂貴的資源,建立和銷燬都須要付出高昂的代價,爲了不頻繁的建立數據庫連接,因此產生了連接池技術。優先在池子中建立一批數據庫連接,有須要訪問數據庫時,直接到池子中去獲取一個可用的連接,使用完了以後再歸還到連接池中去。緩存
一樣的,線程也是一種寶貴的資源,而且也是一種有限的資源,建立和銷燬線程也一樣須要付出不菲的代價。咱們全部的代碼都是由一個一個的線程支撐起來的,現在的芯片架構也決定了咱們必須編寫多線程執行的程序,以獲取最高的程序性能。多線程
那麼怎樣高效的管理多線程之間的分工與協做就成了一個關鍵問題,Doug Lea 大神爲咱們設計並實現了一款線程池工具,經過該工具就能夠實現多線程的能力,並實現任務的高效執行與調度。架構
爲了正確合理的使用線程池工具,咱們有必要對線程池的原理進行了解。工具
本篇文章主要從三個方面來對線程池進行分析:線程池狀態、重要屬性、工做流程。性能
首先線程池是有狀態的,這些狀態標識這線程池內部的一些運行狀況,線程池的開啓到關閉的過程就是線程池狀態的一個流轉的過程。this
線程池共有五種狀態:線程
狀態 | 含義 |
---|---|
RUNNING | 運行狀態,該狀態下線程池能夠接受新的任務,也能夠處理阻塞隊列中的任務<br />執行 shutdown 方法可進入 SHUTDOWN 狀態<br />執行 shutdownNow 方法可進入 STOP 狀態 |
SHUTDOWN | 待關閉狀態,再也不接受新的任務,繼續處理阻塞隊列中的任務<br />當阻塞隊列中的任務爲空,而且工做線程數爲0時,進入 TIDYING 狀態 |
STOP | 中止狀態,不接收新任務,也不處理阻塞隊列中的任務,而且會嘗試結束執行中的任務<br />當工做線程數爲0時,進入 TIDYING 狀態 |
TIDYING | 整理狀態,此時任務都已經執行完畢,而且也沒有工做線程<br />執行 terminated 方法後進入 TERMINATED 狀態 |
TERMINATED | 終止狀態,此時線程池徹底終止了,並完成了全部資源的釋放 |
一個線程池的核心參數有不少,每一個參數都有着特殊的做用,各個參數聚合在一塊兒後將完成整個線程池的完整工做。設計
首先線程池是有狀態的,不一樣狀態下線程池的行爲是不同的,5種狀態已經在上面說過了。對象
另外線程池確定是須要線程去執行具體的任務的,因此在線程池中就封裝了一個內部類 Worker 做爲工做線程,每一個 Worker 中都維持着一個 Thread。
線程池的重點之一就是控制線程資源合理高效的使用,因此必須控制工做線程的個數,因此須要保存當前線程池中工做線程的個數。
看到這裏,你是否以爲須要用兩個變量來保存線程池的狀態和線程池中工做線程的個數呢?可是在 ThreadPoolExecutor 中只用了一個 AtomicInteger 型的變量就保存了這兩個屬性的值,那就是 ctl。
ctl 的高3位用來表示線程池的狀態(runState),低29位用來表示工做線程的個數(workerCnt),爲何要用3位來表示線程池的狀態呢,緣由是線程池一共有5種狀態,而2位只能表示出4種狀況,因此至少須要3位才能表示得了5種狀態。
如今有了標誌工做線程的個數的變量了,那到底該有多少個線程才合適呢?線程多了浪費線程資源,少了又不能發揮線程池的性能。
爲了解決這個問題,線程池設計了兩個變量來協做,分別是:
如今咱們有一個疑問,既然已經有了標識工做線程的個數的變量了,爲何還要有核心線程數、最大線程數呢?
其實你這樣想就可以理解了,建立線程是有代價的,不能每次要執行一個任務時就建立一個線程,可是也不能在任務很是多的時候,只有少許的線程在執行,這樣任務是來不及處理的,而是應該建立合適的足夠多的線程來及時的處理任務。隨着任務數量的變化,當任務數明顯很小時,本來建立的多餘的線程就沒有必要再存活着了,由於這時使用少許的線程就可以處理的過來了,因此說真正工做的線程的數量,是隨着任務的變化而變化的。
那核心線程數和最大線程數與工做線程個數的關係是什麼呢?
工做線程的個數可能從0到最大線程數之間變化,當執行一段時間以後可能維持在 corePoolSize,但也不是絕對的,取決於核心線程是否容許被超時回收。
既然是線程池,那天然少不了線程,線程該如何來建立呢?這個任務就交給了線程工廠 ThreadFactory 來完成。
上面咱們說了核心線程數和最大線程數,而且也介紹了工做線程的個數是在0和最大線程數之間變化的。可是不可能一會兒就建立了全部線程,把線程池裝滿,而是有一個過程,這個過程是這樣的:
當線程池接收到一個任務時,若是工做線程數沒有達到corePoolSize,那麼就會新建一個線程,並綁定該任務,直到工做線程的數量達到 corePoolSize 前都不會重用以前的線程。
當工做線程數達到 corePoolSize 了,這時又接收到新任務時,會將任務存放在一個阻塞隊列中等待覈心線程去執行。爲何不直接建立更多的線程來執行新任務呢,緣由是核心線程中極可能已經有線程執行完本身的任務了,或者有其餘線程立刻就能處理完當前的任務,而且接下來就能投入到新的任務中去,因此阻塞隊列是一種緩衝的機制,給核心線程一個機會讓他們充分發揮本身的能力。另一個值得考慮的緣由是,建立線程畢竟是比較昂貴的,不可能一有任務要執行就去建立一個新的線程。
因此咱們須要爲線程池配備一個阻塞隊列,用來臨時緩存任務,這些任務將等待工做線程來執行。
上面咱們說了當工做線程數達到 corePoolSize 時,線程池會將新接收到的任務存放在阻塞隊列中,而阻塞隊列又兩種狀況:一種是有界的隊列,一種是無界的隊列。
若是是無界隊列,那麼當核心線程都在忙的時候,全部新提交的任務都會被存放在該無界隊列中,這時最大線程數將變得沒有意義,由於阻塞隊列不會存在被裝滿的狀況。
若是是有界隊列,那麼當阻塞隊列中裝滿了等待執行的任務,這時再有新任務提交時,線程池就須要建立新的「臨時」線程來處理,至關於增派人手來處理任務。
可是建立的「臨時」線程是有存活時間的,不可能讓他們一直都存活着,當阻塞隊列中的任務被執行完畢,而且又沒有那麼多新任務被提交時,「臨時」線程就須要被回收銷燬,在被回收銷燬以前等待的這段時間,就是非核心線程的存活時間,也就是 keepAliveTime 屬性。
那麼什麼是「非核心線程」呢?是否是先建立的線程就是核心線程,後建立的就是非核心線程呢?
其實核心線程跟建立的前後沒有關係,而是跟工做線程的個數有關,若是當前工做線程的個數大於核心線程數,那麼全部的線程均可能是「非核心線程」,都有被回收的可能。
一個線程執行完了一個任務後,會去阻塞隊列裏面取新的任務,在取到任務以前它就是一個閒置的線程。
取任務的方法有兩種,一種是經過 take() 方法一直阻塞直到取出任務,另外一種是經過 poll(keepAliveTime,timeUnit) 方法在必定時間內取出任務或者超時,若是超時這個線程就會被回收,請注意核心線程通常不會被回收。
那麼怎麼保證核心線程不會被回收呢?仍是跟工做線程的個數有關,每個線程在取任務的時候,線程池會比較當前的工做線程個數與核心線程數:
因此每一個線程想要保住本身「核心線程」的身份,必須充分努力,儘量快的獲取到任務去執行,這樣才能逃避被回收的命運。
核心線程通常不會被回收,可是也不是絕對的,若是咱們設置了容許核心線程超時被回收的話,那麼就沒有核心線程這種說法了,全部的線程都會經過 poll(keepAliveTime, timeUnit) 來獲取任務,一旦超時獲取不到任務,就會被回收,通常不多會這樣來使用,除非該線程池須要處理的任務很是少,而且頻率也不高,不須要將核心線程一直維持着。
雖然咱們有了阻塞隊列來對任務進行緩存,這從必定程度上爲線程池的執行提供了緩衝期,可是若是是有界的阻塞隊列,那就存在隊列滿的狀況,也存在工做線程的數據已經達到最大線程數的時候。若是這時候再有新的任務提交時,顯然線程池已經愛莫能助了,由於既沒有空餘的隊列空間來存放該任務,也沒法建立新的線程來執行該任務了,因此這時咱們就須要有一種拒絕策略,即 handler。
拒絕策略是一個 RejectedExecutionHandler 類型的變量,用戶能夠自行指定拒絕的策略,若是不指定的話,線程池將使用默認的拒絕策略:拋出異常。
在線程池中還爲咱們提供了不少其餘能夠選擇的拒絕策略:
瞭解了線程池中全部的重要屬性以後,如今咱們須要來了解下線程池的工做流程了。
上圖是一張線程池工做的精簡圖,實際的過程比這個要複雜的多,不過這些應該可以徹底覆蓋到線程池的整個工做流程了。
整個過程能夠拆分紅如下幾個部分:
當向線程池提交一個新的任務時,線程池有三種處理狀況,分別是:建立一個工做線程來執行該任務、將任務加入阻塞隊列、拒絕該任務。
提交任務的過程也能夠拆分紅如下幾個部分:
整個過程能夠用下面這張圖來表示:
建立工做線程須要作一系列的判斷,須要確保當前線程池能夠建立新的線程以後,才能建立。
首先,當線程池的狀態是 SHUTDOWN 或者 STOP 時,則不能建立新的線程。
另外,當線程工廠建立線程失敗時,也不能建立新的線程。
還有就是當前工做線程的數量與核心線程數、最大線程數進行比較,若是前者大於後者的話,也不容許建立。
除此以外,會嘗試經過 CAS 來自增工做線程的個數,若是自增成功了,則會建立新的工做線程,即 Worker 對象。
而後加鎖進行二次驗證是否可以建立工做線程,最後若是建立成功,則會啓動該工做線程。
當工做線程建立成功後,也就是 Worker 對象已經建立好了,這時就須要啓動該工做線程,讓線程開始幹活了,Worker 對象中關聯着一個 Thread,因此要啓動工做線程的話,只要經過 worker.thread.start() 來啓動該線程便可。
啓動完了以後,就會執行 Worker 對象的 run 方法,由於 Worker 實現了 Runnable 接口,因此本質上 Worker 也是一個線程。
經過線程 start 開啓以後就會調用到 Runnable 的 run 方法,在 worker 對象的 run 方法中,調用了 runWorker(this) 方法,也就是把當前對象傳遞給了 runWorker 方法,讓他來執行。
在 runWorker 方法被調用以後,就是執行具體的任務了,首先須要拿到一個能夠執行的任務,而 Worker 對象中默認綁定了一個任務,若是該任務不爲空的話,那麼就是直接執行。
執行完了以後,就會去阻塞隊列中獲取任務來執行,而獲取任務的過程,須要考慮當前工做線程的個數。
逅弈逐碼,專一於原創分享,用通俗易懂的圖文描述源碼及原理