【線程池】如何正確的配置一個線程池


我們在創建自己的線程池時,會時常因為不知道給核心線程數或者最大線程數設置多少為好,其實這個時需要看你的線程池的使用場景和服務器CUP的配置,根據這些前置條件,我們再去判斷如何去設置合適的線程數,並不是我們想設置多少線程數大小就可以設置多少,這樣可能會導致線程發揮不到最大的性能,甚至還有可能會導致服務OOM堆棧溢出的風險。

使用場景

CPU密集型任務

當我們遇到那種需要大量使用CUP的任務時,比如加密、解密、壓縮、計算等一系列操作,這種情況理想的線程核心數是CPU核心數的1~2倍,不宜設置的太多。如果設置太多的線程數,當線程任務越來越多的時候,導致CUP的線程的壓力會越來越大並且線程的數量也會越來越多,導致CPU去切換線程上下文的時間成本會越來越多,這樣不僅不會提升線程的性能,可能還會導致線程的的性能越來越低。

耗時IO型任務

耗時IO其實就是不太耗CUP的資源,只是會在執行上會有一定的等待時間,比如操作數據庫,讀取大文件,或者請求接口等。當我們從數據庫查詢一個很大的數據時,需要等待很久的時間,其實這個等待的時間他並不消耗CPU的資源。所以在這種場景上,我們設置的最大線程數其實可以大於核心線程數很多倍。因為他大部分線程都不怎么消耗線程資源。

計算公式

那么場景有了,我該如何去獲取我們可以設置的核心線程數的值呢?
可以看到下面這個公式

《Java並發編程實戰》的作者 Brain Goetz 推薦的計算方法:

線程數 = CPU 核心數 *(1+平均等待時間/平均工作時間)

可以看到

  • 平均等待時間越長,就代表是耗時IO型任務,這時線程數就會越大。
  • 平均工作時間越長,就代表是CPU密集型任務,這時線程就會越少。

公式有了,但是又缺少線程的時間,執行時間和等待時間我該從哪去獲取呢?那么這個就需要我們去借用一些工具了,想要獲取到准確的時間,就需要進行壓測,然后監控JVM的線程情況以及CPU的負載情況。最后得到他們的時間,然后合理充分的利用資源。

配置參數標准

了解了線程場景后,我們再來看一下,如何通過我們需要的應用場景來設置對於的線程池初始參數。

核心線程數和最大線程數

上面我們說到過,設置合理的線程數,需要看線程的使用場景是CPU密集型任務還是耗時IO型任務。
核心線程數的設置量就按照上面的公式來計算

  • CPU密集型任務:最大線程數為核心線程數的1~2倍
  • 耗時IO型任務:最大線程數為核心線程數的2~5倍(依場景來設定)

那么有沒有通用的設置方法,就是可以適用於CPU密集型任務和耗時IO型任務都適用的設置方法。如果想設置這樣的線程,核心線程數不變,但是最大線程數可以設置的稍微大一點,一般為核心線程數的3~4倍。這種是常用方法,只有在你沒有對線程性能進行壓測的情況下進行設置,如果想獲取最准確的,還是以壓測后的線程狀態,依情況來合理設置。

阻塞隊列

之前我們有介紹過LinkedBlockingQueue,SynchronousQueue,DelayedWorkQueue這三種隊列,具體可以參考
常用的三種阻塞隊列這篇文章,這三種隊列其實都有一個缺點,那就是不好控制線程的的最大數量,這樣可能會導致出現因為隊列中的線程任務太多導致內存爆滿爆出OOM異常。為了避免這種情況,我們再平時設置自己的線程時,都回去使用ArrayBlockingQueue這個阻塞隊列,這個阻塞隊列唯一的好處就是,他可以設置隊列的大小,並且隊列滿了之后,他也不會將塞不進來的線程任務拋棄,而是會阻塞着,然后根據線程池設置的拒絕策略來執行,這樣也就避免了丟失線程任務的風險。那么根據使用場景,我們該如何去設置隊列的容量呢?如果我們使用容量更大的隊列和更小的線程數,就可以減少上下文切換帶來的開銷,但也可能會因此降低整體的吞吐量,如果我們的任務是IO密集型,則可以選擇稍小容量的隊列和更大的最大線程數,這樣整體的線程效率就會高,不過也會帶來更大的上下文切換。我們知道了這個東西之后,其實會發現設置多大的隊列大小,並沒有一個准確的公式,而是慢慢的業務運行的時候慢慢的試錯,當發現我們的容量設置的並不理想,則需要依照當時的情況來調整。

線程工廠

線程工廠的作用其實就是創建線程,如果你對線程池的默認創建線程的方式不滿意,想要對線程池里面的線程名字修改,或者在創建線程的時候去執行一些前置任務,這個時候你就可以自己創建一個線程工廠,示例代碼如下。

public class TestThreadFactory implements ThreadFactory {

    private static AtomicInteger atomicInteger = new AtomicInteger();

    @Override
    public Thread newThread(Runnable r) {
        atomicInteger.addAndGet(1);
        return new Thread(r,"自定義名字-"+atomicInteger.get());
    }
}

拒絕策略

拒絕策略目前有四種,AbortPolicy,DiscardPolicy,DiscardOldestPolicy 或者 CallerRunsPolicy。詳細的作用我們就不說了,可以移步到線程池的4種拒絕策略這篇文章中查看,按照自己的需求來使用合適的拒絕策略,當然,如果你不滿足這四種線程策略,想要自己去整活,線程池也會給你機會,這時你就可以通過實現RejectedExecutionHandler類來實現自己的拒絕策略,示例代碼如下。

public class MyRejectPolicy implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println("被拒絕了");
    }
}

總結

說了這么多,大家可能會發現,其實線程池的創建與配置,並沒有一個統一的標准,文章中給出的一些公式,其實僅僅供於在第一次創建線程池的時候的配置,如果到后面發現,當先創建的線程規則不滿足於現狀,其實這個時候你就可以以自己看到的狀況來定,比如線程執行的時間過長,並且很占用時間,但又不消耗CPU資源,這個時候其實你就會覺得我可以吧最大線程池擴大一點,相反遇到線程執行很慢,並且同時執行的線程又很多,這個時候可能就是遇到了CPU密集執行的情況,這時就可以將最大線程數調小一點,線程的隊列容量再調大一點。這樣下來,可能就會有不一樣的效果。


免責聲明!

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



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