ref:https://www.cnblogs.com/dongguacai/p/6030187.html http://www.360doc.com/content/15/0511/14/12726874_469670444.shtml https://blog.csdn.net/honghail ...
ref:https://www.cnblogs.com/dongguacai/p/6030187.html
http://www.360doc.com/content/15/0511/14/12726874_469670444.shtml
https://blog.csdn.net/honghailiang888/article/details/51690711
http://www.open-open.com/lib/view/open1406778349171.html
https://blog.csdn.net/chaofanwei2/article/details/51393794
http://www.360doc.com/content/15/0511/14/12726874_469670444.shtml
什麼是線程池?
諸如web伺服器、資料庫伺服器、文件伺服器和郵件伺服器等許多伺服器應用都面向處理來自某些遠程來源的大量短小的任務。構建伺服器應用程式的一個過於簡單的模型是:每當一個請求到達就創建一個新的服務對象,然後在新的服務對象中為請求服務。但當有大量請求併發訪問時,伺服器不斷的創建和銷毀對象的開銷很大。所以提高伺服器效率的一個手段就是儘可能減少創建和銷毀對象的次數,特別是一些很耗資源的對象創建和銷毀,這樣就引入了“池”的概念,“池”的概念使得人們可以定製一定量的資源,然後對這些資源進行復用,而不是頻繁的創建和銷毀。
線程池是預先創建線程的一種技術。線程池在還沒有任務到來之前,創建一定數量的線程,放入空閑隊列中。這些線程都是處於睡眠狀態,即均為啟動,不消耗CPU,而只是占用較小的記憶體空間。當請求到來之後,緩衝池給這次請求分配一個空閑線程,把請求傳入此線程中運行,進行處理。當預先創建的線程都處於運行狀態,即預製線程不夠,線程池可以自由創建一定數量的新線程,用於處理更多的請求。當系統比較閑的時候,也可以通過移除一部分一直處於停用狀態的線程。
線程池簡介
多線程技術主要解決處理器單元內多個線程執行的問題,它可以顯著減少處理器單元的閑置時間,增加處理器單元的吞吐能力。
假設一個伺服器完成一項任務所需時間為:T1 創建線程時間,T2 線上程中執行任務的時間,T3 銷毀線程時間。
如果:T1 + T3 遠大於 T2,則可以採用線程池,以提高伺服器性能。
一個線程池包括以下四個基本組成部分:
1、線程池管理器(ThreadPool):用於創建並管理線程池,包括創建線程池,銷毀線程池,添加新任務;
2、工作線程(PoolWorker):線程池中線程,在沒有任務時處於等待狀態,可以迴圈的執行任務;
3、任務介面(Task):每個任務必須實現的介面,以供工作線程調度任務的執行,它主要規定了任務的入口,任務執行完後的收尾工作,任務的執行狀態等;
4、任務隊列(taskQueue):用於存放沒有處理的任務。提供一種緩衝機制。
線程池技術正是關註如何縮短或調整T1,T3時間的技術,從而提高伺服器程式性能的。它把T1,T3分別安排在伺服器程式的啟動和結束的時間段或者一些空閑的時間段,這樣在伺服器程式處理客戶請求時,不會有T1,T3的開銷了。
線程池不僅調整T1,T3產生的時間段,而且它還顯著減少了創建線程的數目,看一個例子:
假設一個伺服器一天要處理50000個請求,並且每個請求需要一個單獨的線程完成。線上程池中,線程數一般是固定的,所以產生線程總數不會超過線程池中線程的數目,而如果伺服器不利用線程池來處理這些請求則線程總數為50000。一般線程池大小是遠小於50000。所以利用線程池的伺服器程式不會為了創建50000而在處理請求時浪費時間,從而提高效率。
代碼實現中並沒有實現任務介面,而是把Runnable對象加入到線程池管理器(ThreadPool),然後剩下的事情就由線程池管理器(ThreadPool)來完成了。
線程池的創建
1 public ThreadPoolExecutor(int corePoolSize, 2 int maximumPoolSize, 3 long keepAliveTime, 4 TimeUnit unit, 5 BlockingQueue<Runnable> workQueue, 6 RejectedExecutionHandler handler)
corePoolSize:線程池核心線程數量
maximumPoolSize:線程池最大線程數量
keepAliverTime:當活躍線程數大於核心線程數時,空閑的多餘線程最大存活時間
unit:存活時間的單位
workQueue:存放任務的隊列
handler:超出線程範圍和隊列容量的任務的處理程式
線程池的實現原理
提交一個任務到線程池中,線程池的處理流程如下:
1、判斷線程池裡的核心線程是否都在執行任務,如果不是(核心線程空閑或者還有核心線程沒有被創建)則創建一個新的工作線程來執行任務。如果核心線程都在執行任務,則進入下個流程。
2、線程池判斷工作隊列是否已滿,如果工作隊列沒有滿,則將新提交的任務存儲在這個工作隊列里。如果工作隊列滿了,則進入下個流程。
3、判斷線程池裡的線程是否都處於工作狀態,如果沒有,則創建一個新的工作線程來執行任務。如果已經滿了,則交給飽和策略來處理這個任務。
圖1 線程池的主要處理流程
java類庫中提供的線程池簡介
java提供的線程池更加強大,相信理解線程池的工作原理,看類庫中的線程池就不會感到陌生了。
圖2 JDK類庫中的線程池的類框圖
圖3 Executors類的生成ExecutorService實例的靜態方法
Java通過Executors提供四種線程池,分別為:
newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
newFixedThreadPool 創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
newScheduledThreadPool 創建一個定長線程池,支持定時及周期性任務執行。
newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。
(1) newCachedThreadPool
創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。示例代碼如下:
- package test;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- public class ThreadPoolExecutorTest {
- public static void main(String[] args) {
- ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
- for (int i = 0; i < 10; i++) {
- final int index = i;
- try {
- Thread.sleep(index * 1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- cachedThreadPool.execute(new Runnable() {
- public void run() {
- System.out.println(index);
- }
- });
- }
- }
- }
線程池為無限大,當執行第二個任務時第一個任務已經完成,會復用執行第一個任務的線程,而不用每次新建線程。
(2) newFixedThreadPool
創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。示例代碼如下:
- package test;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- public class ThreadPoolExecutorTest {
- public static void main(String[] args) {
- ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
- for (int i = 0; i < 10; i++) {
- final int index = i;
- fixedThreadPool.execute(new Runnable() {
- public void run() {
- try {
- System.out.println(index);
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- });
- }
- }
- }
因為線程池大小為3,每個任務輸出index後sleep 2秒,所以每兩秒列印3個數字。
定長線程池的大小最好根據系統資源進行設置。如Runtime.getRuntime().availableProcessors()。
(3) newScheduledThreadPool
創建一個定長線程池,支持定時及周期性任務執行。延遲執行示例代碼如下:
- package test;
- import java.util.concurrent.Executors;
- import java.util.concurrent.ScheduledExecutorService;
- import java.util.concurrent.TimeUnit;
- public class ThreadPoolExecutorTest {
- public static void main(String[] args) {
- ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
- scheduledThreadPool.schedule(new Runnable() {
- public void run() {
- System.out.println("delay 3 seconds");
- }
- }, 3, TimeUnit.SECONDS);
- }
- }
表示延遲1秒後每3秒執行一次。
(4) newSingleThreadExecutor
創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。示例代碼如下:
- package test;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- public class ThreadPoolExecutorTest {
- public static void main(String[] args) {
- ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
- for (int i = 0; i < 10; i++) {
- final int index = i;
- singleThreadExecutor.execute(new Runnable() {
- public void run() {
- try {
- System.out.println(index);
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- });
- }
- }
- }
結果依次輸出,相當於順序執行各個任務。
線程池技術要點
從內部實現上看,線程池技術可主要劃分為如下6個要點實現:
圖4 線程池技術要點圖
- 工作者線程worker:即線程池中可以重覆利用起來執行任務的線程,一個worker的生命周期內會不停的處理多個業務job。線程池“復用”的本質就是復用一個worker去處理多個job,“流控“的本質就是通過對worker數量的控制實現併發數的控制。通過設置不同的參數來控制 worker的數量可以實現線程池的容量伸縮從而實現複雜的業務需求。
- 待處理工作job的存儲隊列:工作者線程workers的數量是有限的,同一時間最多只能處理最多workers數量個job。對於來不及處理的job需要保存到等待隊列里,空閑的工作者work會不停的讀取空閑隊列里的job進行處理。基於不同的隊列實現,可以擴展出多種功能的線程池,如定製隊列出隊順序實現帶處理優先順序的線程池、定製隊列為阻塞有界隊列實現可阻塞能力的線程池等。流控一方面通過控制worker數控制併發數和處理能力,一方面可基於隊列控制線程池處理能力的上限。
- 線程池初始化:即線程池參數的設定和多個工作者workers的初始化。通常有一開始就初始化指定數量的workers或者有請求時逐步初始化工作者兩種方式。前者線程池啟動初期響應會比較快但造成了空載時的少量性能浪費,後者是基於請求量靈活擴容但犧牲了線程池啟動初期性能達不到最優。
- 處理業務job演算法:業務給線程池添加任務job時線程池的處理演算法。有的線程池基於演算法識別直接處理job還是增加工作者數處理job或者放入待處理隊列,也有的線程池會直接將job放入待處理隊列,等待工作者worker去取出執行。
- workers的增減演算法:業務線程數不是持久不變的,有高低峰期。線程池要有自己的演算法根據業務請求頻率高低調節自身工作者workers的 數量來調節線程池大小,從而實現業務高峰期增加工作者數量提高響應速度,而業務低峰期減少工作者數來節省伺服器資源。增加演算法通常基於幾個維度進行:待處 理工作job數、線程池定義的最大最小工作者數、工作者閑置時間。
- 線程池終止邏輯:應用停止時線程池要有自身的停止邏輯,保證所有job都得到執行或者拋棄。
線程池的拒絕策略
池子有對象池如commons pool的GenericObjectPool(通用對象池技術)也有java裡面的線程池ThreadPoolExecutor,但java裡面的線程池引入了一個叫拒絕執行的策略模式,感覺比GenericObjectPool好一點,意思也就是說當池子滿的時候該如何執行還在不斷往裡面添加的一些任務。
像GenericObjectPool只提供了,繼續等待和直接返回空的策略。而ThreadPoolExecutor則提供了一個介面,並內置了4中實現策略供用戶分場景使用。
ThreadPoolExecutor.execute(Runnable command)提供了提交任務的入口,此方法會自動判斷如果池子滿了的話,則會調用拒絕策略來執行此任務,介面為RejectedExecutionHandler,內置的4中策略分別為AbortPolicy、DiscardPolicy、DiscardOldestPolicy、CallerRunsPolicy。
圖5 拒絕策略關係圖
AbortPolicy
為java線程池預設的阻塞策略,不執行此任務,而且直接拋出一個運行時異常,切記ThreadPoolExecutor.execute需要try catch,否則程式會直接退出。
DiscardPolicy
直接拋棄,任務不執行,空方法
DiscardOldestPolicy
從隊列裡面拋棄head的一個任務,並再次execute 此task。
CallerRunsPolicy
在調用execute的線程裡面執行此command,會阻塞入口。
用戶自定義拒絕策略
實現RejectedExecutionHandler,並自己定義策略模式。
再次需要註意的是,ThreadPoolExecutor.submit() 函數,此方法內部調用的execute方法,並把execute執行完後的結果給返回,但如果任務並沒有執行的話(被拒絕了),則submit返回的future.get()會一直等到。
future 內部其實還是一個runnable,並把command給封裝了下,當command執行完後,future會返回一個值。
簡單線程池的設計
一個典型的線程池,應該包括如下幾個部分:
1、線程池管理器(ThreadPool),用於啟動、停用,管理線程池
2、工作線程(WorkThread),線程池中的線程
3、請求介面(WorkRequest),創建請求對象,以供工作線程調度任務的執行
4、請求隊列(RequestQueue),用於存放和提取請求
5、結果隊列(ResultQueue),用於存儲請求執行後返回的結果
線程池管理器,通過添加請求的方法(putRequest)向請求隊列(RequestQueue)添加請求,這些請求事先需要實現請求介面,即傳遞工作函數、參數、結果處理函數、以及異常處理函數。之後初始化一定數量的工作線程,這些線程通過輪詢的方式不斷查看請求隊列(RequestQueue),只要有請求存在,則會提取出請求,進行執行。然後,線程池管理器調用方法(poll)查看結果隊列(resultQueue)是否有值,如果有值,則取出,調用結果處理函數執行。通過以上講述,不難發現,這個系統的核心資源在於請求隊列和結果隊列,工作線程通過輪詢requestQueue獲得人物,主線程通過查看結果隊列,獲得執行結果。因此,對這個隊列的設計,要實現線程同步,以及一定阻塞和超時機制的設計,以防止因為不斷輪詢而導致的過多cpu開銷。
圖6 線程池工作模型
線程池的優點
1、線程是稀缺資源,使用線程池可以減少創建和銷毀線程的次數,每個工作線程都可以重覆使用。
2、可以根據系統的承受能力,調整線程池中工作線程的數量,防止因為消耗過多記憶體導致伺服器崩潰。
線程池的註意事項
雖然線程池是構建多線程應用程式的強大機制,但使用它並不是沒有風險的。在使用線程池時需註意線程池大小與性能的關係,註意併發風險、死鎖、資源不足和線程泄漏等問題。
(1)線程池大小。多線程應用並非線程越多越好,需要根據系統運行的軟硬體環境以及應用本身的特點決定線程池的大小。一般來說,如果代碼結構合理的話,線程數目與CPU 數量相適合即可。如果線程運行時可能出現阻塞現象,可相應增加池的大小;如有必要可採用自適應演算法來動態調整線程池的大小,以提高CPU 的有效利用率和系統的整體性能。
(2)併發錯誤。多線程應用要特別註意併發錯誤,要從邏輯上保證程式的正確性,註意避免死鎖現象的發生。
(3)線程泄漏。這是線程池應用中一個嚴重的問題,當任務執行完畢而線程沒能返回池中就會發生線程泄漏現象。