在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時,選擇更簡單選項通常會帶來最好的、最能預見的性能。