手把手教你手动创建线程池
??點(diǎn)擊上方?好好學(xué)java?,選擇?星標(biāo)?公眾號(hào)
重磅資訊、干貨,第一時(shí)間送達(dá) 今日推薦:2020,搞個(gè) Mac 玩玩!個(gè)人原創(chuàng)+1博客:點(diǎn)擊前往,查看更多 作者:IamHYN 鏈接:https://segmentfault.com/a/1190000021866282一、為什么要手動(dòng)創(chuàng)建線程池?
我們之所以要手動(dòng)創(chuàng)建線程池,是因?yàn)?JDK 自帶的工具類所創(chuàng)建的線程池存在一定的弊端,那究竟存在怎么樣的弊端呢?首先來回顧一下 JDK 中線程池框架的繼承關(guān)系:
Java線程池框架繼承結(jié)構(gòu).png ★JDK 線程池框架繼承關(guān)系圖
”我們最常用的線程池實(shí)現(xiàn)類是ThreadPoolExecutor(紅框里的那個(gè)),首先我們來看一下它最通用的構(gòu)造方法:
/*** 各參數(shù)含義* corePoolSize : 線程池中常駐的線程數(shù)量。核心線程數(shù),默認(rèn)情況下核心線程會(huì)一直存活,即使處于閑置狀態(tài)也不會(huì)* 受存活時(shí)間 keepAliveTime 的限制,除非將 allowCoreThreadTimeOut 設(shè)置為 true。* maximumPoolSize : 線程池所能容納的最大線程數(shù)。超過這個(gè)數(shù)的線程將被阻塞。當(dāng)任務(wù)隊(duì)列為沒有設(shè)置大小的* LinkedBlockingQueue時(shí),這個(gè)值無效。* keepAliveTime : 當(dāng)線程數(shù)量多于 corePoolSize 時(shí),空閑線程的存活時(shí)長,超過這個(gè)時(shí)間就會(huì)被回收* unit : keepAliveTime 的時(shí)間單位* workQueue : 存放待處理任務(wù)的隊(duì)列* threadFactory : 線程工廠* handler : 拒絕策略,拒絕無法接收添加的任務(wù)*/ public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) { ... ... }使用 JDK 自帶的 Executors工具類 (圖中藍(lán)色框中的那個(gè),這是獨(dú)立于線程池繼承關(guān)系圖的工具類,類似于 Collections 和 Arrays) 可以直接創(chuàng)建以下種類的線程池:
線程數(shù)量固定的線程池,此方法返回 ThreadPoolExecutor
單線程線程池,此方法返回 ThreadPoolExecutor
可緩存線程的線程池,此方法返回 ThreadPoolExecutor
執(zhí)行定時(shí)任務(wù)的線程池,此方法返回 ScheduledThreadPoolExecutor
可以拆分執(zhí)行子任務(wù)的線程池,此方法返回 ForkJoinPool
JDK 自帶工具類創(chuàng)建的線程池存在的問題
直接使用這些線程池雖然很方便,但是存在兩個(gè)比較大的問題:
有的線程池可以無限添加任務(wù)或線程,容易導(dǎo)致 OOM;
就拿我們最常用FixedThreadPool和 CachedThreadPool來說,前者的詳細(xì)創(chuàng)建方法如下:
public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());}可見其任務(wù)隊(duì)列用的是LinkedBlockingQueue,且沒有指定容量,相當(dāng)于無界隊(duì)列,這種情況下就可以添加大量的任務(wù),甚至達(dá)到Integer.MAX_VALUE的數(shù)量級(jí),如果任務(wù)大量堆積,可能會(huì)導(dǎo)致 OOM。
而CachedThreadPool的創(chuàng)建方法如下:
public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());}這個(gè)雖然使用了有界隊(duì)列SynchronousQueue,但是最大線程數(shù)設(shè)置成了Integer.MAX_VALUE,這就意味著可以創(chuàng)建大量的線程,也有可能導(dǎo)致 OOM。
還有一個(gè)問題就是這些線程池的線程都是使用 JDK 自帶的線程工廠 (ThreadFactory)創(chuàng)建的,線程名稱都是類似pool-1-thread-1的形式,第一個(gè)數(shù)字是線程池編號(hào),第二個(gè)數(shù)字是線程編號(hào),這樣很不利于系統(tǒng)異常時(shí)排查問題。
如果你安裝了“阿里編碼規(guī)約”的插件,在使用Executors創(chuàng)建線程池時(shí)會(huì)出現(xiàn)以下警告信息:
Alibaba Java Coding Guidelines.png ★阿里編碼規(guī)約的警告信息
”為避免這些問題,我們最好還是手動(dòng)創(chuàng)建線程池。
二、 如何手動(dòng)創(chuàng)建線程池
2.1 定制線程數(shù)量
首先要說明一點(diǎn),定制線程池的線程數(shù)并不是多么高深的學(xué)問,也不是說一旦線程數(shù)設(shè)定不合理,你的程序就無法運(yùn)行,而是要盡量避免以下兩種極端條件:
線程數(shù)量過大
這會(huì)導(dǎo)致過多的線程競爭稀缺的 CPU 和內(nèi)存資源。CPU 核心的數(shù)量和計(jì)算能力是有限的,在分配不到 CPU 執(zhí)行時(shí)間的情況下,線程只能處于空閑狀態(tài)。而在JVM 中,線程本身也是對(duì)象,也會(huì)占用內(nèi)存,過多的空閑線程自然會(huì)浪費(fèi)寶貴的內(nèi)存空間。
線程數(shù)量過小
線程池存在的意義,或者說并發(fā)編程的意義就是為了“壓榨”計(jì)算機(jī)的運(yùn)算能力,說白了就是別讓 CPU 閑著。如果線程數(shù)量比 CPU 核心數(shù)量還小的話,那么必定有 CPU 核心將處于空閑狀態(tài),這是極大的浪費(fèi)。
所以在實(shí)際開發(fā)中我們需要根據(jù)實(shí)際的業(yè)務(wù)場(chǎng)景合理設(shè)定線程池的線程數(shù)量,那又如何分析業(yè)務(wù)場(chǎng)景呢?我們的業(yè)務(wù)場(chǎng)景大致可以分為以下兩大類:
CPU (計(jì)算)密集型
這種場(chǎng)景需要大量的 CPU 計(jì)算,比如加密、計(jì)算 hash 等,最佳線程數(shù)為 (CPU 核心數(shù) + 1)。比如8核 CPU,可以把線程數(shù)設(shè)置為 9,這樣就足夠了,因?yàn)樵?CPU 密集型的場(chǎng)景中,每個(gè)線程都會(huì)在比較大的負(fù)荷下工作,很少出現(xiàn)空閑的情況,正好每個(gè)線程對(duì)應(yīng)一個(gè) CPU 核心,然后不停地工作,這樣就實(shí)現(xiàn)了最優(yōu)利用率。多出的一個(gè)線程起到了備胎的作用,在其他線程意外中斷時(shí)頂替上去,確保 CPU 不中斷工作。其實(shí)也大可不必這么死板,線程數(shù)量設(shè)置為 CPU 核心數(shù)的 1 到 2 倍都是可以接受的。
I/O 密集型
比如讀寫數(shù)據(jù)庫,讀寫文件或者網(wǎng)絡(luò)讀寫等場(chǎng)景。各種 I/O 設(shè)備 (比如磁盤)的速度是遠(yuǎn)低于 CPU 執(zhí)行速度的,所以在 I/O 密集型的場(chǎng)景下,線程大部分時(shí)間都在等待資源而非 CPU 時(shí)間片,這樣的話一個(gè) CPU 核心就可以應(yīng)付很多線程了,也就可以把線程數(shù)量設(shè)置大一點(diǎn)。線程具體數(shù)量的計(jì)算方法可以參考 Brain Goetz 的建議:
假設(shè)有以下變量:
* N<sub>threads</sub> = 線程數(shù)量 * N<sub>cpu</sub> = CPU 核心數(shù) * U<sub>cpu</sub> = 期望的CPU 的使用率 ,因?yàn)?CPU 可能還要執(zhí)行其他任務(wù) * W = 線程的平均等待資源時(shí)間 * C = 線程平均使用 CPU 的計(jì)算時(shí)間 * W / C = 線程等待時(shí)間與計(jì)算時(shí)間的比率這樣為了讓 CPU 達(dá)到期望的使用率,最優(yōu)的線程數(shù)量計(jì)算公式如下:
Nthreads = Ncpu Ucpu ( 1 + W / C )
CPU 核心數(shù)可以通過以下方法獲取:
int N_CPUS = Runtime.getRuntime().availableProcessors();</code>當(dāng)然,除了 CPU,線程數(shù)量還會(huì)受到很多其他因素的影響,比如內(nèi)存和數(shù)據(jù)庫連接等,需要具體問題具體分析。
2.2 使用可自定義線程名稱的線程工廠
這個(gè)就簡單多了,可以借助大名鼎鼎的谷歌開源工具庫 Guava,首先引入如下依賴:
<!-- https://mvnrepository.com/artifact/com.google.guava/guava --> <dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>28.2-jre</version> </dependency>然后我就可以使用其提供的ThreadFactoryBuilder類來創(chuàng)建線程工廠了,Demo 如下:
public class ThreadPoolDemo {// 線程數(shù)public static final int THREAD_POOL_SIZE = 16;public static void main(String[] args) throws InterruptedException {// 使用 ThreadFactoryBuilder 創(chuàng)建自定義線程名稱的 ThreadFactoryThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("hyn-demo-pool-%d").build();// 創(chuàng)建線程池,其中任務(wù)隊(duì)列需要結(jié)合實(shí)際情況設(shè)置合理的容量ThreadPoolExecutor executor = new ThreadPoolExecutor(THREAD_POOL_SIZE,THREAD_POOL_SIZE,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>(1024),namedThreadFactory,new ThreadPoolExecutor.AbortPolicy());// 新建 1000 個(gè)任務(wù),每個(gè)任務(wù)是打印當(dāng)前線程名稱for (int i = 0; i < 1000; i++) {executor.execute(() -> System.out.println(Thread.currentThread().getName()));}// 優(yōu)雅關(guān)閉線程池executor.shutdown();executor.awaitTermination(1000L, TimeUnit.SECONDS);// 任務(wù)執(zhí)行完畢后打印"Done"System.out.println("Done");} }控制臺(tái)打印結(jié)果如下:
... ... hyn-demo-pool-2 hyn-demo-pool-6 hyn-demo-pool-13 hyn-demo-pool-12 hyn-demo-pool-15 Done可見這樣的線程名稱相比pool-1-thread-1更有辨識(shí)度,可以為不同用途的線程池設(shè)定不同的名稱,便于系統(tǒng)出故障時(shí)排查問題。
三、總結(jié)
本文為大家介紹了手動(dòng)創(chuàng)建線程池的詳細(xì)方法,不過這些都是理論性的內(nèi)容,而多線程編程是非常注重實(shí)踐的一門學(xué)問,在實(shí)際生產(chǎn)環(huán)境中要綜合考慮各種因素并不斷嘗試,才能實(shí)現(xiàn)最佳實(shí)踐。
總結(jié)
以上是生活随笔為你收集整理的手把手教你手动创建线程池的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用CMS垃圾收集器产生的问题和解决方案
- 下一篇: 完美实现SpringBoot+Angul