爲何要用線程池1?java
服務器應用程序中常常出現的狀況是:單個任務處理的時間很短而請求的數目倒是巨大的。spring
構建服務器應用程序的一個過於簡單的模型應該是:每當一個請求到達就建立一個新線程,而後在新線程中爲請求服務。實際上,對於原型開發這種方法工做得很好,但若是試圖部署以這種方式運行的服務器應用程序,那麼這種方法的嚴重不足就很明顯。數據庫
每一個請求對應一個線程(thread-per-request)方法的不足之一是:爲每一個請求建立一個新線程的開銷很大;爲每一個請求建立新線程的服務器在建立和銷燬線程上花費的時間和消耗的系統資源要比花在處理實際的用戶請求的時間和資源更多。除了建立和銷燬線程的開銷以外,活動的線程也消耗系統資源(線程的生命週期!)。在一個JVM 裏建立太多的線程可能會致使系統因爲過分消耗內存而用完內存或「切換過分」。爲了防止資源不足,服務器應用程序須要一些辦法來限制任何給定時刻處理的請求數目。編程
線程池爲線程生命週期開銷問題和資源不足問題提供瞭解決方案。經過對多個任務重用線程,線程建立的開銷被分攤到了多個任務上。其好處是,由於在請求到達時線程已經存在,因此無心中也消除了線程建立所帶來的延遲。這樣,就能夠當即爲請求服務,使應用程序響應更快。並且,經過適當地調整線程池中的線程數目,也就是當請求的數目超過某個閾值時,就強制其它任何新到的請求一直等待,直到得到一個線程來處理爲止,從而能夠防止資源不足。服務器
爲何要使用線程池2?多線程
操做系統建立線程、切換線程狀態、終結線程都要進行CPU調度——這是一個耗費時間和系統資源的事情。 併發
大多數實際場景中是這樣的:處理某一次請求的時間是很是短暫的,可是請求數量是巨大的。這種技術背景下,若是咱們爲每個請求都單首創建一個線程,那麼物理機的全部資源基本上都被操做系統建立線程、切換線程狀態、銷燬線程這些操做所佔用,用於業務請求處理的資源反而減小了。因此最理想的處理方式是,將處理請求的線程數量控制在一個範圍,既保證後續的請求不會等待太長時間,又保證物理機將足夠的資源用於請求處理自己。 ide
另外,一些操做系統是有最大線程數量限制的。當運行的線程數量逼近這個值的時候,操做系統會變得不穩定。這也是咱們要限制線程數量的緣由。性能
----------------this
線程池的替代方案
線程池遠不是服務器應用程序內使用多線程的惟一方法。如同上面所提到的,有時,爲每一個新任務生成一個新線程是十分明智的。然而,若是任務建立過於頻繁而任務的平均處理時間太短,那麼爲每一個任務生成一個新線程將會致使性能問題。
另外一個常見的線程模型是爲某一類型的任務分配一個後臺線程與任務隊列。
每一個任務對應一個線程方法和單個後臺線程(single-background-thread)方法在某些情形下都工做得很是理想。每一個任務一個線程方法在只有少許運行時間很長的任務時工做得十分好。而只要調度可預見性不是很重要,則單個後臺線程方法就工做得十分好,如低優先級後臺任務就是這種狀況。然而,大多數服務器應用程序都是面向處理大量的短時間任務或子任務,所以每每但願具備一種可以以低開銷有效地處理這些任務的機制以及一些資源管理和定時可預見性的措施。線程池提供了這些優勢。
----------------
工做隊列
就線程池的實際實現方式而言,術語「線程池」有些令人誤解,由於線程池「明顯的」實如今大多數情形下並不必定產生咱們但願的結果。術語「線程池」先於 Java 平臺出現,所以它多是較少面向對象方法的產物。然而,該術語仍繼續普遍應用着。
雖然咱們能夠輕易地實現一個線程池類,其中客戶機類等待一個可用線程、將任務傳遞給該線程以便執行、而後在任務完成時將線程歸還給池,但這種方法卻存在幾個潛在的負面影響。例如在池爲空時,會發生什麼呢?試圖向池線程傳遞任務的調用者都會發現池爲空,在調用者等待一個可用的池線程時,它的線程將阻塞。咱們之因此要使用後臺線程的緣由之一經常是爲了防止正在提交的線程被阻塞。徹底堵住調用者,如在線程池的「明顯的」實現的狀況,能夠杜絕咱們試圖解決的問題的發生。
咱們一般想要的是同一組固定的工做線程相結合的工做隊列,它使用 wait() 和 notify() 來通知等待線程新的工做已經到達了。該工做隊列一般被實現成具備相關監視器對象的某種鏈表。清單 1 顯示了簡單的合用工做隊列的示例。儘管 Thread API 沒有對使用 Runnable 接口強加特殊要求,但使用 Runnable 對象隊列的這種模式是調度程序和工做隊列的公共約定。
----------------
使用線程池的風險
雖然線程池是構建多線程應用程序的強大機制,但使用它並非沒有風險的。用線程池構建的應用程序容易遭受任何其它多線程應用程序容易遭受的全部併發風險,諸如同步錯誤和死鎖,它還容易遭受特定於線程池的少數其它風險,諸如與池有關的死鎖、資源不足,併發錯誤,線程泄漏,請求過載。
----------------
有效使用線程池的準則
只要您遵循幾條簡單的準則,線程池能夠成爲構建服務器應用程序的極其有效的方法:
(1)不要對那些同步等待其它任務結果的任務排隊。這可能會致使上面所描述的那種形式的死鎖,在那種死鎖中,全部線程都被一些任務所佔用,這些任務依次等待排隊任務的結果,而這些任務又沒法執行,由於全部的線程都很忙。
(2)在爲時間可能很長的操做使用合用的線程時要當心。若是程序必須等待諸如 I/O 完成這樣的某個資源,那麼請指定最長的等待時間,以及隨後是失效仍是將任務從新排隊以便稍後執行。這樣作保證了:經過將某個線程釋放給某個可能成功完成的任務,從而將最終取得 某些 進展。
理解任務。要有效地調整線程池大小,您須要理解正在排隊的任務以及它們正在作什麼。它們是 CPU 限制的(CPU-bound)嗎?它們是 I/O 限制的(I/O-bound)嗎?您的答案將影響您如何調整應用程序。若是您有不一樣的任務類,這些類有着大相徑庭的特徵,那麼爲不一樣任務類設置多個工做隊 列可能會有意義,這樣能夠相應地調整每一個池。
----------------
調整池的大小
調整線程池的大小基本上就是避免兩類錯誤:線程太少或線程太多。幸運的是,對於大多數應用程序來講,太多和太少之間的餘地至關寬。
線程池的最佳大小取決於可用處理器的數目以及工做隊列中的任務的性質。若在一個具備 N 個處理器的系統上只有一個工做隊列,其中所有是計算性質的任務,在線程池具備 N 或 N+1 個線程時通常會得到最大的 CPU 利用率。
處理器利用率不是調整線程池大小過程當中的惟一考慮事項。隨着線程池的增加,您可能會碰到調度程序、可用內存方面的限制,或者其它系統資源方面的限制,例如套接字、打開的文件句柄或數據庫鏈接等的數目。
----------------
無須編寫您本身的池
Doug Lea 編寫了一個優秀的併發實用程序開放源碼庫 util.concurrent ,它包括互斥、信號量、諸如在併發訪問下執行得很好的隊列和散列表之類集合類以及幾個工做隊列實現。該包中的 PooledExecutor 類是一種有效的、普遍使用的以工做隊列爲基礎的線程池的正確實現。
參考:http://blog.csdn.net/ichsonx/article/details/6265071 java 線程池 較詳細文摘
參考:http://www.ibm.com/developerworks/cn/java/l-threadPool/ 線程池的介紹及簡單實現
Java語言爲咱們提供了兩種基礎線程池的選擇:ScheduledThreadPoolExecutor 和 ThreadPoolExecutor。它們都實現了ExecutorService接口(注意,ExecutorService接口自己和「線程池」並無直接關係,它的定義更接近「執行器」,而「使用線程管理的方式進行實現」只是其中的一種實現方式)。這篇文章中,咱們主要圍繞ThreadPoolExecutor類進行講解。
----------------
線程池的建立
咱們能夠經過java.util.concurrent.ThreadPoolExecutor來建立一個線程池。
經常使用構造方法爲:ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler)
處理過程
當一個任務經過execute(Runnable)方法欲添加到線程池時:
處理任務的優先級爲:
核心線程corePoolSize、任務隊列workQueue、最大線程maximumPoolSize,若是三者都滿了,使用handler處理被拒絕的任務。
當線程池中的線程數量大於 corePoolSize時,若是某線程空閒時間超過keepAliveTime,線程將被終止。這樣,線程池能夠動態的調整池中的線程數。
unit可選的參數爲java.util.concurrent.TimeUnit中的幾個靜態屬性: NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。
handler有四個選擇:
舉例:
import java.io.Serializable; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class TestThreadPool2{ public static void main(String[] args){ // 構造一個線程池(池中最少有2個線程,最多4個線程,池中的線程容許空閒3秒,線程池所使用的緩衝隊列ArrayBlockingQueue且容量爲3,線程池對拒絕任務的處理策略爲:拋棄舊的任務) ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 4, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3), new ThreadPoolExecutor.DiscardOldestPolicy()); for (int i = 1; i <= 10; i++){ try{ // 產生一個任務,並將其加入到線程池 String task = "task@ " + i; System.out.println("put " + task); threadPool.execute(new ThreadPoolTask(task)); // 便於觀察,等待一段時間 Thread.sleep(2); } catch (Exception e){ e.printStackTrace(); } } } } /** * 線程池執行的任務 */ class ThreadPoolTask implements Runnable, Serializable{ private static final long serialVersionUID = 0; // 保存任務所須要的數據 private Object threadPoolTaskData; ThreadPoolTask(Object tasks){ this.threadPoolTaskData = tasks; } public void run(){ // 處理一個任務,這裏的處理方式太簡單了,僅僅是一個打印語句 System.out.println(Thread.currentThread().getName()); System.out.println("start .." + threadPoolTaskData); try{ // //便於觀察,等待一段時間 Thread.sleep(1000); }catch (Exception e){ e.printStackTrace(); } threadPoolTaskData = null; } public Object getTask(){ return this.threadPoolTaskData; } }
說明:
這個小組裏面隊員至少有兩個,若是他們兩個忙不過來,任務就被放到任務列表裏面。若是積壓的任務過多,多到任務列表都裝不下(超過3個)的時候,就僱傭新的隊員來幫忙。可是基於成本的考慮,不能僱傭太多的隊員,至多隻能僱傭 4個。若是四個隊員都在忙時,再有新的任務,這個小組就處理不了了,任務就會被經過一種策略來處理,咱們的處理方式是不停的派發,直到接受這個任務爲止(更殘忍!呵呵)。由於隊員工做是須要成本的,若是工做很閒,閒到 3SECONDS都沒有新的任務了,那麼有的隊員就會被解僱了,可是,爲了小組的正常運轉,即便工做再閒,小組的隊員也不能少於兩個。
四、經過調整 produceTaskSleepTime和 consumeTaskSleepTime的大小來實現對派發任務和處理任務的速度的控制,改變這兩個值就能夠觀察不一樣速率下程序的工做狀況。
五、經過調整4中所指的數據,再加上調整任務丟棄策略,換上其餘三種策略,就能夠看出不一樣策略下的不一樣處理方式。
六、對於其餘的使用方法,參看jdk的幫助,很容易理解和使用。
再舉一個例子:
import java.util.Queue; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadPoolExecutorTest { private static int queueDeep = 4; public void createThreadPool(){ /* * 建立線程池,最小線程數爲2,最大線程數爲4,線程池維護線程的空閒時間爲3秒, * 使用隊列深度爲4的有界隊列,若是執行程序還沒有關閉,則位於工做隊列頭部的任務將被刪除, * 而後重試執行程序(若是再次失敗,則重複此過程),裏面已經根據隊列深度對任務加載進行了控制。 */ ThreadPoolExecutor tpe = new ThreadPoolExecutor(2, 4, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueDeep), new ThreadPoolExecutor.DiscardOldestPolicy()); // 向線程池中添加 10 個任務 for (int i = 0; i < 10; i++){ try{ Thread.sleep(1); }catch (InterruptedException e){ e.printStackTrace(); } while (getQueueSize(tpe.getQueue()) >= queueDeep){ System.out.println("隊列已滿,等3秒再添加任務"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } TaskThreadPool ttp = new TaskThreadPool(i); System.out.println("put i:" + i); tpe.execute(ttp); } tpe.shutdown(); } private synchronized int getQueueSize(Queue queue) { return queue.size(); } public static void main(String[] args) { ThreadPoolExecutorTest test = new ThreadPoolExecutorTest(); test.createThreadPool(); } class TaskThreadPool implements Runnable { private int index; public TaskThreadPool(int index) { this.index = index; } public void run() { System.out.println(Thread.currentThread() + " index:" + index); try{ Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
spring線程池ThreadPoolExecutor配置而且獲得任務執行的結果
Java併發包:ExecutorService和ThreadPoolExecutor
Java併發包:Java Fork and Join using ForkJoinPool
Java併發包:ScheduledExecutorService(一種安排任務執行的ExecutorService)
----------------
合理的配置線程池
要想合理的配置線程池,就必須首先分析任務特性,能夠從如下幾個角度來進行分析:
任務性質不一樣的任務能夠用不一樣規模的線程池分開處理。
咱們能夠經過Runtime.getRuntime().availableProcessors()方法得到當前設備的CPU個數。
優先級不一樣的任務可使用優先級隊列PriorityBlockingQueue來處理。
它可讓優先級高的任務先獲得執行,須要注意的是若是一直有優先級高的任務提交到隊列裏,那麼優先級低的任務可能永遠不能執行。
執行時間不一樣的任務能夠交給不一樣規模的線程池來處理,或者也可使用優先級隊列,讓執行時間短的任務先執行。
依賴數據庫鏈接池的任務,由於線程提交SQL後須要等待數據庫返回結果,若是等待的時間越長CPU空閒時間就越長,那麼線程數應該設置越大,這樣才能更好的利用CPU。
建議使用有界隊列,有界隊列能增長系統的穩定性和預警能力,能夠根據須要設大一點,好比幾千。
有一次咱們組使用的後臺任務線程池的隊列和線程池全滿了,不斷的拋出拋棄任務的異常,經過排查發現是數據庫出現了問題,致使執行SQL變得很是緩慢,由於後臺任務線程池裏的任務全是須要向數據庫查詢和插入數據的,因此致使線程池裏的工做線程所有阻塞住,任務積壓在線程池裏。
若是當時咱們設置成無界隊列,線程池的隊列就會愈來愈多,有可能會撐滿內存,致使整個系統不可用,而不僅是後臺任務出現問題。
固然咱們的系統全部的任務是用的單獨的服務器部署的,而咱們使用不一樣規模的線程池跑不一樣類型的任務,可是出現這樣問題時也會影響到其餘任務。