Java ThreadPool的正確打開方式
線程池應對于突然增大、來不及處理的請求,無非兩種應對方式:
- 將未完成的請求放在隊列里等待
- 臨時增加處理線程,等高峰回落后再結束臨時線程 </ol>
JDK的Executors.newFixedPool() 和newCachedPool(),分別使用了這兩種方式。
不過,這倆函數在方便之余,也屏蔽了ThreadPool原本多樣的配置,對一些不求甚解的碼農來說,就錯過了一些更適合自己項目的選擇。
1. ThreadPoolExecutor的原理
經典書《Java Concurrency in Pratice(Java并發編程實戰)》第8章,濃縮如下:
1. 每次提交任務時,如果線程數還沒達到coreSize就創建新線程并綁定該任務。
所以第coreSize次提交任務后線程總數必達到coreSize,不會重用之前的空閑線程。
2. 線程數達到coreSize后,新增的任務就放到工作隊列里,而線程池里的線程則努力的使用take()從工作隊列里拉活來干。
3. 如果隊列是個有界隊列,又如果線程池里的線程不能及時將任務取走,工作隊列可能會滿掉,插入任務就會失敗,此時線程池就會緊急的再創建新的臨時線程來補救。
4. 臨時線程使用poll(keepAliveTime,timeUnit)來從工作隊列拉活,如果時候到了仍然兩手空空沒拉到活,表明它太閑了,就會被解雇掉。
5. 如果core線程數+臨時線程數 >maxSize,則不能再創建新的臨時線程了,轉頭執行RejectExecutionHanlder。默認的AbortPolicy拋 RejectedExecutionException異常,其他選擇包括靜默放棄當前任務(Discard),放棄工作隊列里最老的任務 (DisacardOldest),或由主線程來直接執行(CallerRuns),或你自己發揮想象力寫的一個。
2. FixedPool 與 CachedPool
FixedPool默認用了一條無界的工作隊列 LinkedBlockingQueue, 所以只去到上面的第2步就不會繼續往下走了,coreSize的線程做不完的任務不斷堆積到無限長的Queue中,所以只有coreSize一個參數,其他maxSize,keepAliveTime,RejectHandler的配置都不會實際生效
CachedPool則把coreSize設成0,然后選用了一種特殊的Queue --SynchronousQueue,只要當前沒有空閑線程,Queue就會立刻報插入失敗,讓線程池增加新的臨時線程,默認 KeepAliveTime是1分鐘,而且maxSize是整形的最大值,也就是說只要有干不完的活,都會無限增增加線程數,直到高峰過去線程數才會回落。
3. 對FixedPool的進一步配置
3.1 設置QueueSize
如果不想搞一條無限長的Queue,避免任務無限等待顯得像假死,同時占用太多內存,可能會把它換成一條有界的ArrayBlockingQueue,那就要同時關注一下這條隊列滿了之后的場景,選擇正確的rejectHanlder。
此時,最好還是把maxSize設為coreSize一樣的值,不把臨時線程及其keepAlive時間拉進來,Queue+臨時線程兩者結合聽是好聽,但很難設置好。
4. 對CachedPool的進一步配置
4.1 設置coreSize
coreSize默認為0,但很多時候也希望是一個類似FixedPool的固定值,能處理大部分的情況,不要有太多加加減減的波動,等待和消耗的精力。
4.2 設置maxSize及rejectHandler
同理,maxSize默認是整形最大值,但太多的線程也很可能讓系統崩潰,所以建議還是設一下maxSize和rejectHandler。
4.3 設置keepAliveTime
默認1分鐘,可以根據項目再設置一把。
5. SpringSide的ThreadPoolBuilder
廣告時間,SpringSide的ThreadPoolBuilder能簡化上述的配置。
此文太科普,不是為了幫SpringSide里的Utils打廣告也不會寫