线程池最佳线程数量到底要如何配置?
一、前言
對于從事后端開發的同學來說,線程是必須要使用了,因為使用它可以提升系統的性能。但是,創建線程和銷毀線程都是比較耗時的操作,頻繁的創建和銷毀線程會浪費很多CPU的資源。
此外,如果每個任務都創建一個線程去處理,這樣線程會越來越多。我們知道每個線程默認情況下占1M的內存空間,如果線程非常多,內存資源將會被耗盡。這時,我們需要線程池去管理線程,不會出現內存資源被耗盡的情況,也不會出現頻繁創建和銷毀線程的情況,因為它內部是可以復用線程的。
?
二、從實戰開始
在介紹線程池之前,讓我們先看個例子。
public class MyCallable implements Callable<String> { @Override public String call() throws Exception { System.out.println("MyCallable call");return "success"; } public static void main(String[] args) {ExecutorService threadPool = Executors.newSingleThreadExecutor();try {Future<String> future = threadPool.submit(new MyCallable()); System.out.println(future.get()); } catch (Exception e) { System.out.println(e); } finally { threadPool.shutdown(); }} }這個類的功能就是使用Executors類的newSingleThreadExecutor方法創建了的一個單線程池,他里面會執行Callable線程任務。
三、創建線程池的方法
我們仔細看看Executors類,會發現它里面給我們封裝了不少創建線程池的靜態方法,如下圖所示:
其實,我們總結一下其實只有6種:
1.newCachedThreadPool可緩沖線程池
它的核心線程數是0,最大線程數是integer的最大值,每隔60秒回收一次空閑線程,使用SynchronousQueue隊列。SynchronousQueue隊列比較特殊,內部只包含一個元素,插入元素到隊列的線程被阻塞,直到另一個線程從隊列中獲取了隊列中存儲的元素。同樣,如果線程嘗試獲取元素并且當前不存在任何元素,則該線程將被阻塞,直到線程將元素插入隊列。
2.newFixedThreadPool固定大小線程池
它的核心線程數 和 最大線程數是一樣,都是nThreads變量的值,該變量由用戶自己決定,所以說是固定大小線程池。此外,它的每隔0毫秒回收一次線程,換句話說就是不回收線程,因為它的核心線程數 和 最大線程數是一樣,回收了沒有任何意義。此外,使用了LinkedBlockingQueue隊列,該隊列其實是有界隊列,很多人誤解了,只是它的初始大小比較大是integer的最大值。
3.newScheduledThreadPool定時任務線程池
它的核心線程數是corePoolSize變量,需要用戶自己決定,最大線程數是integer的最大值,同樣,它的每隔0毫秒回收一次線程,換句話說就是不回收線程。使用了DelayedWorkQueue隊列,該隊列具有延時的功能。
4.newSingleThreadExecutor單個線程池
其實,跟上面的newFixedThreadPool是一樣的,稍微有一點區別是核心線程數 和 最大線程數 都是1,這就是為什么說它是單線程池的原因。
5.newSingleThreadScheduledExecutor單線程定時任務線程池
該線程池是對上面介紹過的ScheduledThreadPoolExecutor定時任務線程池的簡單封裝,核心線程數固定是1,其他的功能一模一樣。
6.newWorkStealingPool竊取線程池
它是JDK1.8增加的新線程池,跟其他的實現方式都不一樣,它底層是通過ForkJoinPool類來實現的。會創建一個含有足夠多線程的線程池,來維持相應的并行級別,它會通過工作竊取的方式,使得多核的 CPU 不會閑置,總會有活著的線程讓 CPU 去運行。
講了這么多,具體要怎么用呢?
其實newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor 和 newWorkStealingPool方法創建和使用線程池的方法是一樣的。這四個方法創建線程池返回值是ExecutorService,通過它的execute方法執行線程。
public class MyWorker implements Runnable {@Override public void run() {System.out.println("MyWorker run");}public static void main(String[] args) {ExecutorService threadPool = Executors.newFixedThreadPool(8); try {threadPool.execute(new MyWorker());} catch (Exception e) {System.out.println(e);} finally {threadPool.shutdown();}} }newScheduledThreadPool 和 newSingleThreadScheduledExecutor 方法創建和使用線程池的方法也是一樣的
以上兩個方法創建的線程池返回值是ScheduledExecutorService,通過它的schedule提交線程,并且可以配置延遲執行的時間。
?
四、自定義線程池
Executors類有這么多方法可以創建線程池,但是阿里巴巴開發規范中卻明確規定不要使用Executors類創建線程池,這是為什么呢?
newCachedThreadPool可緩沖線程池,它的最大線程數是integer的最大值,意味著使用它創建的線程池,可以創建非常多的線程,我們都知道一個線程默認情況下占用內存1M,如果創建的線程太多,占用內存太大,最后肯定會出現內存溢出的問題。
newFixedThreadPool和newSingleThreadExecutor在這里都稱為固定大小線程池,它的隊列使用的LinkedBlockingQueue,我們都知道這個隊列默認大小是integer的最大值,意味著可以往該隊列中加非常多的任務,每個任務也是要內存空間的,如果任務太多,最后肯定也會出現內存溢出的問題。
阿里建議使用ThreadPoolExecutor類創建線程池,其實從剛剛看到的Executors類創建線程池的newFixedThreadPool等方法可以看出,它也是使用ThreadPoolExecutor類創建線程池的。
從上圖可以看出ThreadPoolExecutor類的構造方法有4個,里面包含了很多參數,讓我們先一起認識一下:
corePoolSize:核心線程數 maximumPoolSize:最大線程數 keepAliveTime:空閑線程回收時間間隔 unit:空閑線程回收時間間隔單位 workQueue:提交任務的隊列,當線程數量超過核心線程數時,可以將任務提交到任務隊列中。比較常用的有:ArrayBlockingQueue; LinkedBlockingQueue; SynchronousQueue; threadFactory:線程工廠,可以自定義線程的一些屬性,比如:名稱或者守護線程等 handler:表示當拒絕處理任務時的策略 ThreadPoolExecutor.AbortPolicy:丟棄任務并拋出RejectedExecutionException異常。 ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。 ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然后重新嘗試執行任務(重復此過程) ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務我們根據上面的內容自定義一個線程池:
從上面可以看到,我們使用ThreadPoolExecutor類自定義了一個線程池,它的核心線程數是8,最大線程數是 10,空閑線程回收時間是30,單位是秒,存放任務的隊列用的ArrayBlockingQueue,而隊列滿的處理策略用的AbortPolicy。使用這個隊列,基本可以保持線程在系統的可控范圍之內,不會出現內存溢出的問題。但是也不是絕對的,只是出現內存溢出的概率比較小。
當然,阿里巴巴開發規范建議不使用Executors類創建線程池,并不表示它完全沒用,在一些低并發的業務場景照樣可以使用。
五、最佳線程數
在使用線程池時,很多同學都有這樣的疑問,不知道如何配置線程數量,今天我們一起探討一下這個問題。
1.經驗值
配置線程數量之前,首先要看任務的類型是 IO密集型,還是CPU密集型?
什么是IO密集型?
比如:頻繁讀取磁盤上的數據,或者需要通過網絡遠程調用接口。
什么是CPU密集型?
比如:非常復雜的調用,循環次數很多,或者遞歸調用層次很深等。
IO密集型配置線程數經驗值是:2N,其中N代表CPU核數。
CPU密集型配置線程數經驗值是:N + 1,其中N代表CPU核數。
如果獲取N的值?
int availableProcessors = Runtime.getRuntime().availableProcessors();那么問題來了,混合型(既包含IO密集型,又包含CPU密集型)的如何配置線程數?
混合型如果IO密集型,和CPU密集型的執行時間相差不太大,可以拆分開,以便于更好配置。如果執行時間相差太大,優化的意義不大,比如IO密集型耗時60s,CPU密集型耗時1s。
2.最佳線程數目算法
除了上面介紹是經驗值之外,其實還提供了計算公式:
最佳線程數目 = ((線程等待時間+線程CPU時間)/線程CPU時間 )* CPU數目很顯然線程等待時間所占比例越高,需要越多線程。線程CPU時間所占比例越高,需要越少線程。
雖說最佳線程數目算法更準確,但是線程等待時間和線程CPU時間不好測量,實際情況使用得比較少,一般用經驗值就差不多了。再配合系統壓測,基本可以確定最適合的線程數。
有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
總結
以上是生活随笔為你收集整理的线程池最佳线程数量到底要如何配置?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python基础知识一
- 下一篇: 字符与字符串3——char 的大小