常用線程池的工作原理和使用場景

1.1 爲何需要使用線程池

       線程的創建和銷燬,都涉及到系統調用,比較消耗系統資源,所以就引入了線程池技術,避免頻繁的線程創建和銷燬。說白了就是爲了避免頻繁創建和銷燬線程帶來的巨大開銷。

       比如說,線程池裏面比如說固定就是100個線程,後面的人過來了就是從線程池裏獲取線程來工作,線程幹完活兒了,別銷燬,直接還到線程池裏去,避免頻繁的創建和銷燬線程,會導致系統的運行效率很低。

Java線程池包含4個部分

(1)線程池管理器(ThreadPool):就是負責創建和銷燬線程池的

(2)工作線程(PoolWorker):就是線程池中的一個線程

(3)工作任務(Task):這個就是線程池裏的某個線程需要執行的業務代碼,這個是你自己編寫的業務邏輯

(4)任務隊列(TaskQueue):這個是扔到線程池裏的任務需要進行排隊,要進任務隊列

1.2 常用的幾種線程池和API

常用的線程池有:

(1)SingleThreadExecutor(單線程線程池,自己做一個內存隊列 -> 啓動後臺線程去消費)

(2)FixedThreadExecutor(固定數量線程池):比如說,線程池裏面固定就100個線程,超過這個線程數就到隊列裏面去排隊等待

(3)CachedThreadExecutor(自動回收空閒線程,根據需要自動新增線程,傳說中的無界線程池):無論有多少任務,根據你的需要,無限制的創建任意多的線程,在最短的時間內來滿足你,但是高峯過去之後,如果有大量的線程處於空閒狀態,沒有活兒可以幹,等待60s之後空閒的線程就被銷燬了

(4)ScheduledThreadExecutor(線程數量無限制,支持定時調度執行某個線程):提交一個任務,對於這個任務不是立馬執行的,是可以設定一個定時調度的邏輯,比如說每隔60s執行一次,這個一般不用,一般來說就用spring schedule的支持

Java的線程池比較重要的幾個API:

(1)Executor:代表線程池的接口,有個execute()方法,扔進去一個Runnable類型對象,就可以分配一個線程給你執行

(2)ExecutorService:這是Executor的子接口,相當於是一個線程池的接口,有銷燬線程池等方法 -> ExecutorService就代表了一個線程池管理器,會負責管理線程池 -> 線程的創建和銷燬 -> 隊列排隊

(3)Executors:線程池的輔助工具類,輔助入口類,可以通過Executors來快捷的創建你需要的線程池。創建線程池的入口類,包含newSingleThreadExecutor()、newCachedThreadPool()、newScheduleThreadPool()、newFixedThreadPool(),這些方法,就是可以讓你創建不同的線程池出來

(4)ThreadPoolExecutor:這是ExecutorService的實現類,這纔是正兒八經代表一個線程池的類,一般在Executors裏創建線程池的時候,內部都是直接創建一個ThreadPoolExecutor的實例對象返回的,然後同時給設置了各種默認參數。

如果我們要創建一個線程池,兩種方式,要麼就是Executors.newXX()方法,快捷的創建一個線程池出來,線程池的所有參數設置都採取默認的方式;要麼是自己手動構建一個THreadPoolExecutor的一個對象,所有的線程池的參數,都可以自己手動來調整和設置

    public class Executors {

    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>());   
    }	

    }

1.3 線程池的構造參數和真正的工作原理

ThreadPoolExecutor(int corePoolSize, 
                int maximumPoolSize, 
                long keepAliveTime, 
                TimeUnit unit, 
                BlockingQueue<Runnable> workQueue, 
                ThreadFactory threadFactory, 
                RejectedExecutionHandler handler)

ThreadPoolExecutor就是線程池,那麼這個類的構造函數的所有入參,就是你可以設置的參數,我們來解釋一下這些參數吧

corePoolSize:線程池裏的核心線程數量

maximumPoolSize:線程池裏允許有的最大線程數量

keepAliveTime:如果線程數量大於corePoolSize的時候,多出來的線程會等待指定的時間之後就被釋放掉,這個就是用來設置空閒線程等待時間的

unit:這個是上面那個keepAliveTime的單位

workQueue:這個是說,通過ThreadPoolExecutor.execute()方法扔進來的Runnable工作任務,會進入一個隊列裏面去排隊,這就是那個隊列

threadFactory:如果需要創建新的線程放入線程池的時候,就是通過這個線程工廠來創建的

handler:假如說上面那個workQueue是有固定大小的,如果往隊列裏扔的任務數量超過了隊列大小,咋辦?就用這個handler來處理,AbortPolicy、DiscardPolicy、DiscardOldestPolicy,如果說線程都繁忙,隊列還滿了,此時就會報錯,RejectException

這些參數的含義先解釋一下:

假設我們自己手動創建一個ThreadPoolExecutor線程池,設置了以下的一些參數

corePoolSize:2個

mamximumPoolSize:4個

keepAliveTime:60s

workQueue:ArrayBlockingQueue,有界阻塞隊列,隊列大小是4

handler:默認的策略,拋出來一個ThreadPoolRejectException

(1)一開始線程池裏的線程是空的,一個都沒有。有一個變量維護的是當前線程數量,這個變量是poolSize,poolSize = 0,如果當前線程的數量小於corePoolSize(2),poolSize < corePoolSize,那麼來了一個任務優先創建線程,直到線程池裏的線程數量跟corePoolSize一樣;poolSize = 1,poolSize < corePoolSize(2),又創建一個線程來處理這個任務;poolSize = 2

(2)如果當前線程池的線程數量(poolSize = 2)大於等於corePoolSize(2)的時候,而且任務隊列沒滿(最大大小是4,但是當前元素數量是0),那麼就扔到任務隊列裏去

(3)如果當前線程池的線程數量大於等於corePoolSize的時候,而且任務隊列滿了(最大大小是4,當前已經放了4個元素了,已經滿了),那麼如果當前線程數量小於最大線程數(poolSize = 2,maimumPoolSize = 4,poolSize < maximumPoolSize),就繼續創建線程;poolSize = 3,提交了一個任務,poolSize >= corePoolSize,任務隊列滿,poolSize < maximumPoolSize,再次創建一個任務

(4)如果此時poolSize >= corePoolSize,任務隊列滿,poolSize == maximumPoolSize,此時再次提交一個任務,當前線程數已經達到了最大線程數了,那麼就使用handler來處理,默認是拋出異常,ThreadPoolRejectExeception

(5)此時線程池裏有4個線程,都處於空閒狀態,corePoolSize指定的是就2個線程就可以了,但是此時超過了corePoolSize 2個線程,所以如果那超出的2個線程空閒時間超過了60s,然後線程池就會將超出的2個線程給回收掉

如何設置池的這些參數?先來看看創建線程池的默認代碼

其實上面都說過了,啥時候會創建新線程?其實就是線程數沒到corePoolSize的時候,會創建線程;接着就是任務隊列滿了,但是線程數小於maximumPoolSize的時候,也會創建線程;創建的時候通過threadFactory來創建即可

1.4 常用線程池的工作原理

FixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {  

         return new ThreadPoolExecutor(nThreads, nThreads,  

                                                                      0L, TimeUnit.MILLISECONDS,  

                                                                      new LinkedBlockingQueue<Runnable>());  

}

(1)corePoolSize = 100,maximumPoolSize = 100,keepAliveTime = 0,workQueue = 無界隊列。

(2)剛開始的時候,比如說假設一開始線程池裏沒有線程,你就不斷的提交任務,瞬間提交了100個任務,一下子創建100個線程出來,其實poolSize == corePoolSize,再提交任務,直接就會發現LinkedBlockQueue根本就沒有大小的限制,所以說根本就不會滿,所以此時後續的所有任務直接扔到LinkedBlockingQueue裏面去排隊。

(3)100個線程,只要出現了空閒,就會從隊列裏面去獲取任務來處理,以此類推,就100個線程,不停的處理任務。

(4)LinkedBlockingQueue根本就不會滿,直到扔任務扔的內存溢出,扔了幾百萬個任務,幾千萬個任務,隊列實在是太大太大了,導致內存溢出,滿了,就死了。

(5)maximumPoolSize根本就沒用,而且其實也是直接跟corePoolSize設置成一樣。

(6)keepAliveTime = 0,也就是,創建出來的線程,根本就不會去判斷是否超過了指定的空閒時間,不會去回收空閒線程數量,後面就維護這麼固定數量的一個線程,有任務就往隊列裏面懟,固定數量的比如100個線程不停的處理任務,如果100個線程都處理不過來,那麼就無限制的往LinkedBlockingQueue裏面去排隊,直到內存溢出。

CachedThreadPool

public static ExecutorService newCachedThreadPool() {   
		 return new ThreadPoolExecutor(0, Integer.MAX_VALUE,   
                                       60L, TimeUnit.SECONDS,   
                                       new SynchronousQueue<Runnable>());   
}
  1. corePoolSize = 0,maximumPoolSize = 最大值(無限大),keepAliveTime = 60s,workQueue=SynchronousQueue
  2. SynchronousQueue(實際上沒有存儲數據的空閒,是用來做多線程通信之間的協調作用的),一開始提交一個任務過來,要求線程池裏必須有一個線程對應可以處理這個任務,但是此時一個線程都沒有,poolSize >= corePoolSize , workQueue已經滿了,poolSize < maximumPoolSize(最大值),直接就會創建一個新的線程來處理這個任務
  3. 如果短期內有大量的任務都涌進來,實際上是走一個直接提交的思路,對每個任務,如果沒法找到一個空閒的線程來處理它,那麼就會立即創建一個新的線程出來,來處理這個新提交的任務
  4. 短時間內,如果大量的任務涌入,可能會導致瞬間創建出來幾百個線程,幾千個線程,是不固定的
  5. 但是當這些線程工作完一段時間之後,就會處於空閒狀態,就會看超過60s的空閒,就會直接將空閒的線程給釋放掉

總結一下:

(1)FixedThreadPool:線程數量是固定的,如果所有線程都繁忙,後續任務全部在一個無界隊列裏面排隊,無限任務來排隊,直到內存溢出

(2)CachcedThreadPool:線程數量是不固定的,如果一下子涌入大量任務,沒有空閒線程,那麼就創建新的線程來處理;如果有空閒線程,就是一個任務配對一個空閒線程來處理;如果線程空閒時間 超過60s,就給回收掉空閒線程。

1.5 各種線程池在什麼樣的場景下使用

FixedThreadPool:比較適用於什麼場景呢?負載比較重,而且負載比較穩定的這麼一個場景,我給大家來舉個例子,我們之前線上有一套系統,負載比較重,後臺系統,每分鐘要執行幾百個複雜的大SQL,就是用FixedThreadPool是最合適的。因爲負載穩定,所以一般來說,不會出現說突然瞬間涌入大量的請求,100個線程處理不過來,然後就直接無限制的排隊,然後oom內存溢出,死了

CachedThreadPool:負載很穩定的場景,用CachedThreadPool就浪費了;每天大部分時候可能就是負載很低的,用少量的線程就可以滿足低負載,不會給系統引入太大的壓力;但是每天如果有少量的高峯期,比如說中午或者是晚上,高峯期可能需要一下子搞幾百個線程出來,那麼CachedThreadPool就可以滿足這個場景;高峯期應付過去之後,線程如果處於空閒狀態超過60s,自動回收空閒線程,避免給系統帶來過大的負載。

1.6 如何設置線程池的參數

corePoolSize:這個其實就是說你算一下每個任務要耗費多少時間,比如一個任務大概100ms,那麼每個線程每秒可以處理10個任務,然後你算算你每秒總共要處理多少個任務啊,比如說200個任務,那麼你就需要20個線程,每個線程每秒處理10個任務,不就可以處理200個任務。但是一般都會多設置一些,比如你可以設置個30個線程。

        希望用類似於FixedThreadPool的這個線程池,corePoolSize和maximumPoolSize按照上面說的策略設置成一樣的就可以了,如果用的是FixedPool的話,一般在於workQueue和handler的理解,因爲你看下默認的實現,其實線程數量達到corePoolSize的時候,就會放入workQueue排隊,但是默認使用的是無界隊列,LinkedBlockingQueue,所以會無限制往裏面排隊,然後就是你corePooSize指定數量的線程不斷的處理,隊列裏的任務可能會無限制的增加。這個其實就是適合處理那種長期不斷有大量任務進來,長期負載都很重,所以你不能用CachedPool,否則長期讓機器運行大量線程,可能導致機器死掉,cpu耗盡。所以你就只能控制線程的數量,用有限的線程不斷的處理源源不斷進入的任務,有時高峯時任務較多,就做一下排隊即可。所以FixedPool的參數裏,對於workQueue,你要考慮一點,默認的是無界隊列,可能會有問題,就是要是無限排隊,別把機器給搞死了,那麼這個時候你可以換成ArrayBlockingQueue,就是有界隊列,自己設置一個最大長度,一旦超出了最大長度,就通過handler去處理,你可以自己對handler接口實現自己的邏輯,我給你舉個例子,此時你可以把數據放到比如數據庫裏去,做離線存儲或者是什麼的。

       希望用類似於CachedThreadPool的這麼一個策略corePoolSize可以設置爲0,但是maximumPoolSize考慮不用設置成無限大,有一個風險,假設突然進來的流量高峯,導致你的線程池一下子出來了幾萬個線程,瞬間會打滿cpu負載,直接機器會死掉maximumPoolSize可以設置成一個,你的機器cpu能負載的最大的線程數,一個經驗值,4核8G的虛擬機,你線程池啓動的線程數量達到100個就差不多了,如果同時有100個線程,而且做很頻繁的操作,cpu可能就快到70%,80%,90%corePoolSize = 0,maximumPoolSize = 150 -> handler報錯 -> 實現一個handler,將多餘的線程給離線存儲起來,後續高峯過了,再重新掃描出來重新提交,你看下CachedPool,他那裏用的是SynchronousQueue,這個queue的意思是如果要插入一個任務,必須有一個任務已經被消費掉了,所以很可能出現說,線程數量達到corePoolSize之後,大量的任務進來,此時SynchronousQueue裏的任務如果還沒被拿走,那麼就會認爲隊列滿了,此時就會創建新的線程,但是maximumPoolSize是無限大的,所以會無限制的創建新的線程。但是如果後續有線程空閒了,那麼就會被回收掉。所以如果你用CachedPool,相當於是在高峯期,無限制的創建線程來拼命耗盡你的機器資源來處理併發涌入的大量的任務。所以CachedPool,可以用在那種瞬時併發任務較高,但是每個任務耗時都較短的場景,就是短時間內突然來個小高峯,那麼就快速啓動大量線程來處理,但是每個線程處理都很快,而且高峯很快就過去了

1.7 線程池關閉原理

shutdown():調用之後不允許提交新的任務了,所有調用之前提交的任務都會執行,等所有任務執行完了,纔會真正關閉線程池,這就是優雅的關閉方式

shutdownNow():返回還沒執行的task列表,然後不讓等待的task執行,嘗試停止正在執行的task,非優雅關閉,強制關閉

如有雷同侵權,請聯繫刪除。

發佈了9 篇原創文章 · 獲贊 17 · 訪問量 5241
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章