線程池

線程池

什麼是線程池
線程池的組成部分
線程池的實現原理
線程池的應用場景
使用線程池的優缺點

什麼是線程池

線程池是一種多線程處理形式,處理過程中將任務添加到隊列,然後在創建線程後自動啓動這些任務

線程池類圖

這裏寫圖片描述

線程池重要的類

這裏寫圖片描述
Executor 接口只有一個 方法,execute,並且需要 傳入一個 Runnable 類型的參數
ExecutorService 接口繼承了 Executor,並且提供了一些其他的方法
shutdownNow : 關閉線程池,返回放入了線程池,但是還沒開始執行的線程。
submit : 執行的任務 允許擁有返回值。
invokeAll : 運行把任務放進集合中,進行批量的執行,並且能有返回
AbstractExecutorService 是一個抽象類,主要完成了 對 submit 方法,invokeAll 方法 的實現
ThreadPoolExecutor 繼承了 AbstractExecutorService,並且實現了最重要的 execute 方法

線程池的組成部分

1、線程池管理器(ThreadPool):用於創建並管理線程池,包括 創建線程池,銷燬線程池,添加新任務;
2、工作線程(PoolWorker):線程池中線程,在沒有任務時處於等待狀態,可以循環的執行任務;
3、任務接口(Task):每個任務必須實現的接口,以供工作線程調度任務的執行,它主要規定了任務的入口,任務執行完後的收尾工作,任務的執行狀態等;
4、任務隊列(taskQueue):用於存放沒有處理的任務。提供一種緩衝機制。

線程池實現原理

1.線程池狀態

2.任務的執行

3.線程池中的線程初始化

4.任務緩存隊列及排隊策略

5.任務拒絕策略

6.線程池的關閉

7.線程池容量的動態調整

線程池狀態

RUNNING :接受提交任務
SHUTDOWN:關閉狀態 不在接受但能處理
STOP:不能接受新任務,也不處理隊列中的任務,會中斷正在處理任務的線程
TIDYING:有效線程數)爲0
TERMINATED:在terminated() 方法執行完後進入該狀態

// 得到線程數,也就是後29位的數字。 直接跟CAPACITY做一個與操作即可,CAPACITY就是的值就 1 << 29 - 1 = 00011111111111111111111111111111。 與操作的話前面3位肯定爲0,相當於直接取後29位的值
private static int workerCountOf(int c) { return c & CAPACITY; }

// 得到狀態,CAPACITY的非操作得到的二進制位11100000000000000000000000000000,然後做在一個與操作,相當於直接取前3位的的值
private static int runStateOf(int c) { return c & ~CAPACITY; }

// 或操作。相當於更新數量和狀態兩個操作
private static int ctlOf(int rs, int wc) { return rs | wc; }
詳情 https://fangjian0423.github.io/2016/03/22/java-threadpool-analysis/

線程池狀態轉換

這裏寫圖片描述

execute方法執行流程

這裏寫圖片描述

線程池中的線程初始化

prestartCoreThread():初始化一個核心線程;
prestartAllCoreThreads():初始化所有核心線程

任務緩存隊列及排隊策略

workQueue,它用來存放等待執行的任務。
workQueue的類型爲BlockingQueue,通常可以取下面三種類型:
1)ArrayBlockingQueue:基於數組的先進先出隊列,此隊列創建時必須指定大小;

2)LinkedBlockingQueue:基於鏈表的先進先出隊列,如果創建時沒有指定此隊列大小,則默認爲Integer.MAX_VALUE;

3)synchronousQueue:這個隊列比較特殊,它不會保存提交的任務,而是將直接新建一個線程來執行新來的任務
(有限隊列、無線隊列、隊列 、棧)

任務拒絕策略

當線程池的任務緩存隊列已滿並且線程池中的線程數目達到maximumPoolSize,如果還有任務到來就會採取任務拒絕策略,通常有以下四種策略:

ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。
ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務

線程池的關閉

ThreadPoolExecutor提供了兩個方法,用於線程池的關閉,分別是shutdown()和shutdownNow(),其中:

shutdown():不會立即終止線程池,而是要等所有任務緩存隊列中的任務都執行完後才終止,但再也不會接受新的任務
shutdownNow():立即終止線程池,並嘗試打斷正在執行的任務,並且清空任務緩存隊列,返回尚未執行的任務

線程池容量的動態調整

ThreadPoolExecutor提供了動態調整線程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize(),

setCorePoolSize:設置核心池大小
setMaximumPoolSize:設置線程池最大能創建的線程數目大小

線程池的創建

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);
創建一個線程池需要輸入幾個參數:

corePoolSize(線程池的基本大小):當提交一個任務到線程池時,線程池會創建一個線程來執行任務,即使其他空閒的基本線程能夠執行新任務也會創建線程,等到需要執行的任務數大於線程池基本大小時就不再創建。如果調用了線程池的prestartAllCoreThreads方法,線程池會提前創建並啓動所有基本線程。
runnableTaskQueue(任務隊列):用於保存等待執行的任務的阻塞隊列。 可以選擇以下幾個阻塞隊列。
ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。
LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列。
SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。
PriorityBlockingQueue:一個具有優先級的無限阻塞隊列。
maximumPoolSize(線程池最大大小):線程池允許創建的最大線程數。如果隊列滿了,並且已創建的線程數小於最大線程數,則線程池會再創建新的線程執行任務。值得注意的是如果使用了無界的任務隊列這個參數就沒什麼效果。
ThreadFactory:用於設置創建線程的工廠,可以通過線程工廠給每個創建出來的線程設置更有意義的名字。
RejectedExecutionHandler(飽和策略):當隊列和線程池都滿了,說明線程池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略默認情況下是AbortPolicy,表示無法處理新任務時拋出異常。以下是JDK1.5提供的四種策略。
AbortPolicy:直接拋出異常。
CallerRunsPolicy:只用調用者所在線程來運行任務。
DiscardOldestPolicy:丟棄隊列裏最近的一個任務,並執行當前任務。
DiscardPolicy:不處理,丟棄掉。
當然也可以根據應用場景需要來實現RejectedExecutionHandler接口自定義策略。如記錄日誌或持久化不能處理的任務。
keepAliveTime(線程活動保持時間):線程池的工作線程空閒後,保持存活的時間。所以如果任務很多,並且每個任務執行的時間比較短,可以調大這個時間,提高線程的利用率。
TimeUnit(線程活動保持時間的單位):可選的單位有天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
四種實現
1.newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。
2.newFixedThreadPool 創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
3.newScheduledThreadPool 創建一個定長線程池,支持定時及週期性任務執行。
4.newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。

線程池的應用場景

單個任務處理時間比較短
需要處理的任務數量很大

線程池的優缺點

線程複用
控制最大併發數
管理線程
缺點、死鎖、資源不足、線程泄露、併發錯誤、請求過載
使用線程池的風險
雖然線程池是構建多線程應用程序的強大機制,但使用它並不是沒有風險的。用線程池構建的應用程序容易遭受任何其它多線程應用程序容易遭受的所有併發風險,諸如同步錯誤和死鎖,它還容易遭受特定於線程池的少數其它風險,諸如與池有關的死鎖、資源不足和線程泄漏。
1。死鎖
任何多線程應用程序都有死鎖風險。當一組進程或線程中的每一個都在等待一個只有該組中另一個進程才能引起的事件時,我們就說這組進程或線程死鎖 了。死鎖的最簡單情形是:線程 A 持有對象 X 的獨佔鎖,並且在等待對象 Y 的鎖,而線程 B 持有對象 Y 的獨佔鎖,卻在等待對象 X 的鎖。除非有某種方法來打破對鎖的等待(Java 鎖定不支持這種方法),否則死鎖的線程將永遠等下去。
雖然任何多線程程序中都有死鎖的風險,但線程池卻引入了另一種死鎖可能,在那種情況下,所有池線程都在執行已阻塞的等待隊列中另一任務的執行結果的任務, 但這一任務卻因爲沒有未被佔用的線程而不能運行。當線程池被用來實現涉及許多交互對象的模擬,被模擬的對象可以相互發送查詢,這些查詢接下來作爲排隊的任 務執行,查詢對象又同步等待着響應時,會發生這種情況。
2.資源不足
線程池的一個優點在於:相對於其它替代調度機制(有些我們已經討論過)而言,它們通常執行得很好。但只有恰當地調整了線程池大小時纔是這樣的。線程消耗包括內存和其它系統資源在內的大量資源。除了Thread 對象所需的內存之外,每個線程都需要兩個可能很大的執行調用堆棧。除此以外,JVM 可能會爲每個 Java 線程創建一個本機線程,這些本機線程將消耗額外的系統資源。最後,雖然線程之間切換的調度開銷很小,但如果有很多線程,環境切換也可能嚴重地影響程序的性能。
如果線程池太大,那麼被那些線程消耗的資源可能嚴重地影響系統性能。在線程之間進行切換將會浪費時間,而且使用超出比您實際需要的線程可能會引起資源匱乏 問題,因爲池線程正在消耗一些資源,而這些資源可能會被其它任務更有效地利用。除了線程自身所使用的資源以外,服務請求時所做的工作可能需要其它資源,例 如 JDBC 連接、套接字或文件。這些也都是有限資源,有太多的併發請求也可能引起失效,例如不能分配 JDBC 連接。
3、併發錯誤
線程池和其它排隊機制依靠使用wait() 和 notify() 方法,這兩個方法都難於使用。如果編碼不正確,那麼可能丟失通知,導致線程保持空閒狀態,儘管隊列中有工作要處理。使用這些方法時,必須格外小心;即便是專家也可能在它們上面出錯。而最好使用現有的、已經知道能工作的實現,例如在下面的 無需編寫自己的線程池中討論的util.concurrent 包。
4、線程泄漏
各種類型的線程池中一個嚴重的風險是線程泄漏,當從池中除去一個線程以執行一項任務,而在任務完成後該線程卻沒有返回池時,會發生這種情況。發生線程泄漏的一種情形出現在任務拋出一個RuntimeException 或一個 Error 時。如果池類沒有捕捉到它們,那麼線程只會退出而線程池的大小將會永久減少一個。當這種情況發生的次數足夠多時,線程池最終就爲空,而且系統將停止,因爲沒有可用的線程來處理任務。
有些任務可能會永遠等待某些資源或來自用戶的輸入,而這些資源又不能保證變得可用,用戶可能也已經回家了,諸如此類的任務會永久停止,而這些停止的任務也 會引起和線程泄漏同樣的問題。如果某個線程被這樣一個任務永久地消耗着,那麼它實際上就被從池除去了。對於這樣的任務,應該要麼只給予它們自己的線程,要 麼只讓它們等待有限的時間。
請求過載
僅僅是請求就壓垮了服務器,這種情況是可能的。在這種情形下,我們可能不想將每個到來的請求都排隊到我們的工作隊列,因爲排在隊列中等待執行的任務可能會 消耗太多的系統資源並引起資源缺乏。在這種情形下決定如何做取決於您自己;在某些情況下,您可以簡單地拋棄請求,依靠更高級別的協議稍後重試請求,您也可 以用一個指出服務器暫時很忙的響應來拒絕請求。

有效使用線程池的準則

只要您遵循幾條簡單的準則,線程池可以成爲構建服務器應用程序的極其有效的方法:
• 不要對那些同步等待其它任務結果的任務排隊。這可能會導致上面所描述的那種形式的死鎖,在那種死鎖中,所有線程都被一些任務所佔用,這些任務依次等待排隊任務的結果,而這些任務又無法執行,因爲所有的線程都很忙。
• 在爲時間可能很長的操作使用合用的線程時要小心。如果程序必須等待諸如 I/O 完成這樣的某個資源,那麼請指定最長的等待時間,以及隨後是失效還是將任務重新排隊以便稍後執行。這樣做保證了:通過將某個線程釋放給某個可能成功完成的任務,從而將最終取得某些 進展。
• 理解任務。要有效地調整線程池大小,您需要理解正在排隊的任務以及它們正在做什麼。它們是 CPU 限制的(CPU-bound)嗎?它們是 I/O 限制的(I/O-bound)嗎?您的答案將影響您如何調整應用程序。如果您有不同的任務類,這些類有着截然不同的特徵,那麼爲不同任務類設置多個工作隊 列可能會有意義,這樣可以相應地調整每個池。

問題

工作線程數是不是設置的越大越好?
調用sleep()函數的時候,線程是否一直佔用CPU?
單核CPU,設置多線程有意義麼,是否能提高併發性能

線程數不是越多越好
sleep()不佔用CPU
單核設置多線程不但能使得代碼清晰,還能提高吞吐量

N核服務器,通過日誌分析出任務執行過程中,本地計算時間爲x,等待時間爲y,則工作線程數(線程池線程數)設置爲 N*(x+y)/x,能讓CPU的利用率最大化

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