五、為什么不應(yīng)該自動創(chuàng)建線程池

所謂的自動創(chuàng)建線程池就是直接調(diào)用 Executors 的各種方法來生成前面學過的常見的線程池,例如 Executors.newCachedThreadPool()。但這樣做是有一定風險的,接下來我們就來逐一分析自動創(chuàng)建線程池可能帶來哪些問題。

FixedThreadPool

首先我們來看第一種線程池 FixedThreadPool, 它是線程數(shù)量固定的線程池,如源碼所示,newFixedThreadPool 內(nèi)部實際還是調(diào)用了 ThreadPoolExecutor 構(gòu)造函數(shù)。

public static ExecutorService newFixedThreadPool(int nThreads) { 
    return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}

通過往構(gòu)造函數(shù)中傳參,創(chuàng)建了一個核心線程數(shù)和最大線程數(shù)相等的線程池,它們的數(shù)量也就是我們傳入的參數(shù),這里的重點是使用的隊列是容量沒有上限的 LinkedBlockingQueue,如果我們對任務(wù)的處理速度比較慢,那么隨著請求的增多,隊列中堆積的任務(wù)也會越來越多,最終大量堆積的任務(wù)會占用大量內(nèi)存,并發(fā)生 OOM ,也就是OutOfMemoryError,這幾乎會影響到整個程序,會造成很嚴重的后果。

SingleThreadExecutor

第二種線程池是 SingleThreadExecutor,我們來分析下創(chuàng)建它的源碼。

public static ExecutorService newSingleThreadExecutor() { 
    return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}

可以看出,newSingleThreadExecutor 和 newFixedThreadPool 的原理是一樣的,只不過把核心線程數(shù)和最大線程數(shù)都直接設(shè)置成了 1,但是任務(wù)隊列仍是無界的 LinkedBlockingQueue,所以也會導致同樣的問題,也就是當任務(wù)堆積時,可能會占用大量的內(nèi)存并導致 OOM。

CachedThreadPool

第三種線程池是 CachedThreadPool,創(chuàng)建它的源碼下所示。

public static ExecutorService newCachedThreadPool() { 
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}

這里的 CachedThreadPool 和前面兩種線程池不一樣的地方在于任務(wù)隊列使用的是 SynchronousQueue,SynchronousQueue 本身并不存儲任務(wù),而是對任務(wù)直接進行轉(zhuǎn)發(fā),這本身是沒有問題的,但你會發(fā)現(xiàn)構(gòu)造函數(shù)的第二個參數(shù)被設(shè)置成了 Integer.MAX_VALUE,這個參數(shù)的含義是最大線程數(shù),所以由于 CachedThreadPool 并不限制線程的數(shù)量,當任務(wù)數(shù)量特別多的時候,就可能會導致創(chuàng)建非常多的線程,最終超過了操作系統(tǒng)的上限而無法創(chuàng)建新線程,或者導致內(nèi)存不足。

ScheduledThreadPool 和 SingleThreadScheduledExecutor

第四種線程池 ScheduledThreadPool 和第五種線程池 SingleThreadScheduledExecutor 的原理是一樣的,創(chuàng)建 ScheduledThreadPool 的源碼如下所示。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { 
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

而這里的 ScheduledThreadPoolExecutor 是 ThreadPoolExecutor 的子類,調(diào)用的它的構(gòu)造方法如下所示。

public ScheduledThreadPoolExecutor(int corePoolSize) { 
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());
}

我們通過源碼可以看出,它采用的任務(wù)隊列是 DelayedWorkQueue,這是一個延遲隊列,同時也是一個無界隊列,所以和 LinkedBlockingQueue 一樣,如果隊列中存放過多的任務(wù),就可能導致 OOM。

你可以看到,這幾種自動創(chuàng)建的線程池都存在風險,相比較而言,我們自己手動創(chuàng)建會更好,因為我們可以更加明確線程池的運行規(guī)則,不僅可以選擇適合自己的線程數(shù)量,更可以在必要的時候拒絕新任務(wù)的提交,避免資源耗盡的風險。

合適的線程數(shù)量是多少,以及 CPU 核心數(shù)和線程數(shù)的關(guān)系。

你可能經(jīng)常在面試中被問到這兩個問題,如果想要很好地回答它們首先你需要了解,我們調(diào)整線程池中的線程數(shù)量的最主要的目的是為了充分并合理地使用 CPU 和內(nèi)存等資源,從而最大限度地提高程序的性能。在實際工作中,我們需要根據(jù)任務(wù)類型的不同選擇對應(yīng)的策略。

CPU 密集型任務(wù)

首先,我們來看 CPU 密集型任務(wù),比如加密、解密、壓縮、計算等一系列需要大量耗費 CPU 資源的任務(wù)。對于這樣的任務(wù)最佳的線程數(shù)為 CPU 核心數(shù)的 1~2 倍,如果設(shè)置過多的線程數(shù),實際上并不會起到很好的效果。此時假設(shè)我們設(shè)置的線程數(shù)量是 CPU 核心數(shù)的 2 倍以上,因為計算任務(wù)非常重,會占用大量的 CPU 資源,所以這時 CPU 的每個核心工作基本都是滿負荷的,而我們又設(shè)置了過多的線程,每個線程都想去利用 CPU 資源來執(zhí)行自己的任務(wù),這就會造成不必要的上下文切換,此時線程數(shù)的增多并沒有讓性能提升,反而由于線程數(shù)量過多會導致性能下降。

針對這種情況,我們最好還要同時考慮在同一臺機器上還有哪些其他會占用過多 CPU 資源的程序在運行,然后對資源使用做整體的平衡。

耗時 IO 型任務(wù)

第二種任務(wù)是耗時 IO 型,比如數(shù)據(jù)庫、文件的讀寫,網(wǎng)絡(luò)通信等任務(wù),這種任務(wù)的特點是并不會特別消耗 CPU 資源,但是 IO 操作很耗時,總體會占用比較多的時間。對于這種任務(wù)最大線程數(shù)一般會大于 CPU 核心數(shù)很多倍,因為 IO 讀寫速度相比于 CPU 的速度而言是比較慢的,如果我們設(shè)置過少的線程數(shù),就可能導致 CPU 資源的浪費。而如果我們設(shè)置更多的線程數(shù),那么當一部分線程正在等待 IO 的時候,它們此時并不需要 CPU 來計算,那么另外的線程便可以利用 CPU 去執(zhí)行其他的任務(wù),互不影響,這樣的話在任務(wù)隊列中等待的任務(wù)就會減少,可以更好地利用資源。

《Java并發(fā)編程實戰(zhàn)》的作者 Brain Goetz 推薦的計算方法:

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

通過這個公式,我們可以計算出一個合理的線程數(shù)量,如果任務(wù)的平均等待時間長,線程數(shù)就隨之增加,而如果平均工作時間長,也就是對于我們上面的 CPU 密集型任務(wù),線程數(shù)就隨之減少。

太少的線程數(shù)會使得程序整體性能降低,而過多的線程也會消耗內(nèi)存等其他資源,所以如果想要更準確的話,可以進行壓測,監(jiān)控 JVM 的線程情況以及 CPU 的負載情況,根據(jù)實際情況衡量應(yīng)該創(chuàng)建的線程數(shù),合理并充分利用資源。

綜上所述我們就可以得出一個結(jié)論:

1.線程的平均工作時間所占比例越高,就需要越少的線程;

2.線程的平均等待時間所占比例越高,就需要越多的線程;

3.針對不同的程序,進行對應(yīng)的實際測試就可以得到最合適的選擇。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容