JAVA線程池調優


    在JAVA中,線程可以使用定制的代碼來管理,應用也可以利用線程池。在使用線程池時,有一個因素非常關鍵:調節線程池的大小對獲得最好的性能至關重要。線程池的性能會隨線程池大小這一基本選擇而有所不同,在某些條件下,線程池過大對性能也有很多不利的影響。

    所有線程池的工作方式本質是一樣的:有一個任務隊列,一定數量的線程會從該任務隊列獲取任務然后執行。任務的結果可以發回客戶端,或保存到數據庫,或保存到某個內部數據結構中,等等。但是在執行完任務后,這個線程會返回任務隊列,檢索另一個任務並執行。

    線程池有最小線程數和最大線程數。池中會有最小數目的線程隨時待命,等待任務指派給它們。因為創建線程的成本非常高昂,這樣可以提高任務提交時的整體性能。線程池的最小線程數稱作核心池大小,考慮ThreadPoolExecutor最簡單的情況,如果有個任務要執行,而所有的並發線程都在忙於執行另一個任務,就會啟動一個新線程,直到創建的線程達到最大線程數。

    一般我們會從以下幾個方面對線程池進行設置:

  • 設置最大線程數

    對於給定硬件上的給定負載,最大線程數設置為多少最好呢?這個問題回答起來並不簡單:它取決於負載特性以及底層硬件。特別是,最優線程數還與每個任務阻塞的頻率有關。

    假設JVM有4個CPU可用,很明顯最大線程數至少要設置為4。的確,除了處理這些任務,JVM還有些線程要做其他的事,但是它們幾乎從來不會占用一個完整的CPU,至於這個數值是否要大於4,則需要進行大量充分的測試。

    有以下兩點需要注意:

    一旦服務器成為瓶頸,向服務器增加負載時非常有害的;

    對於CPU密集型或IO密集型的機器增加線程數實際會降低整體的吞吐量;

  • 設置最小線程數

    一旦確定了線程池的最大線程數,就該確定所需的最小線程數了。大部分情況下,開發者會直截了當的將他們設置成同一個值。

    將最小線程數設置為其他某個值(比如1),出發點是為了防止系統創建太多線程,以節省系統資源。指定一個最小線程數的負面影響相當小。如果第一次就有很多任務要執行,會有負面影響:這是線程池需要創建一個新線程。創建線程對性能不利,這也是為什么起初需要線程池的原因。

    一般而言,對於線程數為最小值的線程池,一個新線程一旦創建出來,至少應該保留幾分鍾,以處理任何負載飆升。空閑時間應該以分鍾計,而且至少在10分鍾到30分鍾之間,這樣可以防止頻繁創建線程。

  • 線程池任務大小

    等待線程池來執行的任務會被保存到某個隊列或列表中;當池中有線程可以執行任務時,就從隊列中拉出一個。這會導致不均衡:隊列中任務的數量可能變得非常大。如果隊列太大,其中的任務就必須等待很長時間,直到前面的任務執行完畢。

    對於任務隊列,線程池通常會限制其大小。但是這個值應該如何調優,並沒有一個通用的規則。若要確定哪個值能帶來我們需要的性能,測量我們的真實應用是唯一的途徑。不管是哪種情況,如果達到了隊列限制,再添加任務就會失敗。ThreadPoolExecutor有一個rejectedExecution方法,用於處理這種情況,默認會拋出RejectedExecutionExecption。應用服務器會向用戶返回某個錯誤:或者是HTTP狀態碼500,或者是Web服務器捕獲異常錯誤,並向用戶給出合理的解釋消息—其中后者是最理想的。

  • 設置ThreadPoolExecutor的大小

    線程池的一般行為是這樣的:創建時准備最小數目的線程,如果來了一個任務,而此時所有的線程都在忙碌,則啟動一個新線程(一直到達到最大線程數),任務就會立即執行。否則,任務被加入到等待隊列,如果隊列中已經無法加入新任務,則拒接之。

    根據所選任務隊列的類型,ThreadPoolExecutor會決定何時會啟動一個新線程。有以下三種可能:

    • SynchronousQueue

      如果ThreadPoolExecutor搭配的是SynchronousQueue,則線程池的行為和我們預期的一樣,它會考慮線程數:如果所有的線程都在忙碌,而且池中的線程數尚未達到最大,則會為新任務啟動一個新線程。然而這個隊列沒辦法保存等待的任務:如果來了一個任務,創建的線程數已經達到最大值,而且所有的線程都在忙碌,則新的任務都會被拒絕,所以如果是管理少量的任務,這是個不錯的選擇,對於其他的情況就不適合了。

    • 無界隊列

      如果ThreadPoolExecutor搭配的是無界隊列,如LinkedBlockingQueue,則不會拒絕任何任務(因為隊列大小沒有限制)。這種情況下,ThreadPoolExecutor最多僅會按照最小線程數創建線程,也就是說最大線程池大小被忽略了。如果最大線程數和最小線程數相同,則這種選擇和配置了固定線程數的傳統線程池運行機制最為接近。

    • 有界隊列

      搭配了有界隊列,如ArrayBlockingQueue的ThreadPoolExecutor會采用一個非常負責的算法。比如假定線程池的最小線程數為4,最大為8所用的ArrayBlockingQueue最大為10。隨着任務到達並被放到隊列中,線程池中最多運行4個線程(即最小線程數)。即使隊列完全填滿,也就是說有10個處於等待狀態的任務,ThreadPoolExecutor也只會利用4個線程。

      如果隊列已滿,而又有新任務進來,此時才會啟動一個新線程,這里不會因為隊列已滿而拒接該任務,相反會啟動一個新線程。新線程會運行隊列中的第一個任務,為新來的任務騰出空間。

      這個算法背后的理念是:該池大部分時間僅使用核心線程(4個),即使有適量的任務在隊列中等待運行。這時線程池就可以用作節流閥。如果擠壓的請求變得非常多,這時該池就會嘗試運行更多的線程來清理;這時第二個節流閥—最大線程數就起作用了。

    對於上面提到的每一種選擇,都能找到很多支持或反對的依據,但是在嘗試獲得最好的性能時,可以應用KISS原則"Keep it simple,stupid"。可以將最小線程數和最大線程數設置為相同,在保存任務方面,如果適合無界隊列,則選擇LinkedBlockingQueue;如果適合有界隊列,則選擇ArrayBlockingQueue。

 

小結:

 

  • 有時對象池也是不錯的選擇,線程池就是情形之一:線程初始化的成本很高,線程池使得系統上的線程數容易控制。
  • 線程池必須需仔細調優,盲目的向池中添加新線程,在某些情況下對性能會有不利的影響。
  • 使用ThreadPoolExecutor時,選擇更簡單選項通常會帶來最好的、最能預見的性能。

     

     

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM