今天跟大家聊一聊無(wú)論是在工作中常用還是在面試中常問(wèn)的線程池,通過(guò)畫(huà)圖的方式來(lái)徹底弄懂線程池的工作原理,以及在實(shí)際項(xiàng)目中該如何自定義適合業(yè)務(wù)的線程池。
一、什么是線程池
線程池其實(shí)是一種池化的技術(shù)的實(shí)現(xiàn),池化技術(shù)的核心思想其實(shí)就是實(shí)現(xiàn)資源的一個(gè)復(fù)用,避免資源的重復(fù)創(chuàng)建和銷毀帶來(lái)的性能開(kāi)銷。在線程池中,線程池可以管理一堆線程,讓線程執(zhí)行完任務(wù)之后不會(huì)進(jìn)行銷毀,而是繼續(xù)去處理其它線程已經(jīng)提交的任務(wù)。
線程池的好處:
降低資源消耗。通過(guò)重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗。
提高響應(yīng)速度。當(dāng)任務(wù)到達(dá)時(shí),任務(wù)可以不需要等到線程創(chuàng)建就能立即執(zhí)行。
提高線程的可管理性。線程是稀缺資源,如果無(wú)限制的創(chuàng)建,不僅會(huì)消耗系統(tǒng)資源,還會(huì)降低系統(tǒng) 的穩(wěn)定性,使用線程池可以進(jìn)行統(tǒng)一的分配,調(diào)優(yōu)和監(jiān)控。
基于 Spring Boot + MyBatis Plus + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
項(xiàng)目地址:https://github.com/YunaiV/ruoyi-vue-pro
視頻教程:https://doc.iocoder.cn/video/
二、線程池的構(gòu)造
Java中主要是通過(guò)構(gòu)建ThreadPoolExecutor來(lái)創(chuàng)建線程池的,接下來(lái)我們看一下線程池是如何構(gòu)造出來(lái)的。
線程池構(gòu)造參數(shù)
corePoolSize:線程池中用來(lái)工作的核心的線程數(shù)量。
maximumPoolSize:最大線程數(shù),線程池允許創(chuàng)建的最大線程數(shù)。
keepAliveTime:超出 corePoolSize 后創(chuàng)建的線程存活時(shí)間或者是所有線程最大存活時(shí)間,取決于配置。
unit:keepAliveTime 的時(shí)間單位。
workQueue:任務(wù)隊(duì)列,是一個(gè)阻塞隊(duì)列,當(dāng)線程數(shù)已達(dá)到核心線程數(shù),會(huì)將任務(wù)存儲(chǔ)在阻塞隊(duì)列中。
threadFactory :線程池內(nèi)部創(chuàng)建線程所用的工廠。
handler:拒絕策略;當(dāng)隊(duì)列已滿并且線程數(shù)量達(dá)到最大線程數(shù)量時(shí),會(huì)調(diào)用該方法處理該任務(wù)。
線程池的構(gòu)造其實(shí)很簡(jiǎn)單,就是傳入一堆參數(shù),然后進(jìn)行簡(jiǎn)單的賦值操作。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
項(xiàng)目地址:https://github.com/YunaiV/yudao-cloud
視頻教程:https://doc.iocoder.cn/video/
三、線程池的運(yùn)行原理
說(shuō)完線程池的核心構(gòu)造參數(shù)的意思,接下來(lái)就來(lái)畫(huà)圖講解這些參數(shù)在線程池中是如何工作的。
線程池剛創(chuàng)建出來(lái)是什么樣子呢,如下圖
不錯(cuò),剛創(chuàng)建出來(lái)的線程池中只有一個(gè)構(gòu)造時(shí)傳入的阻塞隊(duì)列而已,此時(shí)里面并沒(méi)有的任何線程,但是如果你想要在執(zhí)行之前已經(jīng)創(chuàng)建好核心線程數(shù),可以調(diào)用prestartAllCoreThreads方法來(lái)實(shí)現(xiàn),默認(rèn)是沒(méi)有線程的。
當(dāng)有線程通過(guò)execute方法提交了一個(gè)任務(wù),會(huì)發(fā)生什么呢?
提交任務(wù)的時(shí)候,其實(shí)會(huì)去進(jìn)行任務(wù)的處理
首先會(huì)去判斷當(dāng)前線程池的線程數(shù)是否小于核心線程數(shù),也就是線程池構(gòu)造時(shí)傳入的參數(shù)corePoolSize。
如果小于,那么就直接通過(guò)ThreadFactory創(chuàng)建一個(gè)線程來(lái)執(zhí)行這個(gè)任務(wù),如圖
當(dāng)任務(wù)執(zhí)行完之后,線程不會(huì)退出,而是會(huì)去從阻塞隊(duì)列中獲取任務(wù),如下圖
接下來(lái)如果又提交了一個(gè)任務(wù),也會(huì)按照上述的步驟,去判斷是否小于核心線程數(shù),如果小于,還是會(huì)創(chuàng)建線程來(lái)執(zhí)行任務(wù),執(zhí)行完之后也會(huì)從阻塞隊(duì)列中獲取任務(wù)。這里有個(gè)細(xì)節(jié),就是提交任務(wù)的時(shí)候,就算有線程池里的線程從阻塞隊(duì)列中獲取不到任務(wù),如果線程池里的線程數(shù)還是小于核心線程數(shù),那么依然會(huì)繼續(xù)創(chuàng)建線程,而不是復(fù)用已有的線程。
如果線程池里的線程數(shù)不再小于核心線程數(shù)呢?那么此時(shí)就會(huì)嘗試將任務(wù)放入阻塞隊(duì)列中,入隊(duì)成功之后,如圖
這樣在阻塞的線程就可以獲取到任務(wù)了。
但是,隨著任務(wù)越來(lái)越多,隊(duì)列已經(jīng)滿了,任務(wù)放入失敗了,那怎么辦呢?
此時(shí)就會(huì)判斷當(dāng)前線程池里的線程數(shù)是否小于最大線程數(shù),也就是入?yún)r(shí)的maximumPoolSize參數(shù)
如果小于最大線程數(shù),那么也會(huì)創(chuàng)建非核心線程來(lái)執(zhí)行提交的任務(wù),如圖
所以,從這里可以發(fā)現(xiàn),就算隊(duì)列中有任務(wù),新創(chuàng)建的線程還是優(yōu)先處理這個(gè)提交的任務(wù),而不是從隊(duì)列中獲取已有的任務(wù)執(zhí)行,從這可以看出,先提交的任務(wù)不一定先執(zhí)行。
但是不幸的事發(fā)生了,線程數(shù)已經(jīng)達(dá)到了最大線程數(shù)量,那么此時(shí)會(huì)怎么辦呢?
此時(shí)就會(huì)執(zhí)行拒絕策略,也就是構(gòu)造線程池的時(shí)候,傳入的RejectedExecutionHandler對(duì)象,來(lái)處理這個(gè)任務(wù)。
RejectedExecutionHandler的實(shí)現(xiàn)JDK自帶的默認(rèn)有4種
AbortPolicy:丟棄任務(wù),拋出運(yùn)行時(shí)異常
CallerRunsPolicy:由提交任務(wù)的線程來(lái)執(zhí)行任務(wù)
DiscardPolicy:丟棄這個(gè)任務(wù),但是不拋異常
DiscardOldestPolicy:從隊(duì)列中剔除最先進(jìn)入隊(duì)列的任務(wù),然后再次提交任務(wù)
線程池創(chuàng)建的時(shí)候,如果不指定拒絕策略就默認(rèn)是AbortPolicy策略。當(dāng)然,你也可以自己實(shí)現(xiàn)RejectedExecutionHandler接口,比如將任務(wù)存在數(shù)據(jù)庫(kù)或者緩存中,這樣就數(shù)據(jù)庫(kù)或者緩存中獲取到被拒絕掉的任務(wù)了。
到這里,我們發(fā)現(xiàn),線程池構(gòu)造的幾個(gè)參數(shù)corePoolSize、maximumPoolSize、workQueue、threadFactory、handler我們都在上述的執(zhí)行過(guò)程中講到了,那么還差兩個(gè)參數(shù)keepAliveTime和unit(unit是keepAliveTime的時(shí)間單位)沒(méi)講到,所以keepAliveTime是如何起到作用的呢,這個(gè)問(wèn)題留到后面分析。
說(shuō)完整個(gè)執(zhí)行的流程,接下來(lái)看看execute方法代碼是如何實(shí)現(xiàn)的。
execute方法
workerCountOf(c)
workQueue.offer(command):這行代碼就表示嘗試往阻塞隊(duì)列中添加任務(wù)
添加失敗之后就會(huì)再次調(diào)用addWorker方法嘗試添加非核心線程來(lái)執(zhí)行任務(wù)
如果還是添加非核心線程失敗了,那么就會(huì)調(diào)用reject(command)來(lái)拒絕這個(gè)任務(wù)。
最后再來(lái)另畫(huà)一張圖總結(jié)execute執(zhí)行流程
四、線程池中線程實(shí)現(xiàn)復(fù)用的原理
線程池的核心功能就是實(shí)現(xiàn)了線程的重復(fù)利用,那么線程池是如何實(shí)現(xiàn)線程的復(fù)用呢?
線程在線程池內(nèi)部其實(shí)是被封裝成一個(gè)Worker對(duì)象
Worker繼承了AQS,也就是有一定鎖的特性。
創(chuàng)建線程來(lái)執(zhí)行任務(wù)的方法上面提到是通過(guò)addWorker方法創(chuàng)建的。在創(chuàng)建Worker對(duì)象的時(shí)候,會(huì)把線程和任務(wù)一起封裝到Worker內(nèi)部,然后調(diào)用runWorker方法來(lái)讓線程執(zhí)行任務(wù),接下來(lái)我們就來(lái)看一下runWorker方法。
啟動(dòng)線程處理任務(wù)
從這張圖可以看出線程執(zhí)行完任務(wù)不會(huì)退出的原因,runWorker內(nèi)部使用了while死循環(huán),當(dāng)?shù)谝粋€(gè)任務(wù)執(zhí)行完之后,會(huì)不斷地通過(guò)getTask方法獲取任務(wù),只要能獲取到任務(wù),就會(huì)調(diào)用run方法,繼續(xù)執(zhí)行任務(wù),這就是線程能夠復(fù)用的主要原因。
但是如果從getTask獲取不到方法的時(shí)候,最后就會(huì)調(diào)用finally中的processWorkerExit方法,來(lái)將線程退出。
這里有個(gè)一個(gè)細(xì)節(jié)就是,因?yàn)閃orker繼承了AQS,每次在執(zhí)行任務(wù)之前都會(huì)調(diào)用Worker的lock方法,執(zhí)行完任務(wù)之后,會(huì)調(diào)用unlock方法,這樣做的目的就可以通過(guò)Woker的加鎖狀態(tài)就能判斷出當(dāng)前線程是否正在運(yùn)行任務(wù)。如果想知道線程是否正在運(yùn)行任務(wù),只需要調(diào)用Woker的tryLock方法,根據(jù)是否加鎖成功就能判斷,加鎖成功說(shuō)明當(dāng)前線程沒(méi)有加鎖,也就沒(méi)有執(zhí)行任務(wù)了,在調(diào)用shutdown方法關(guān)閉線程池的時(shí)候,就用這種方式來(lái)判斷線程有沒(méi)有在執(zhí)行任務(wù),如果沒(méi)有的話,來(lái)嘗試打斷沒(méi)有執(zhí)行任務(wù)的線程。
五、線程是如何獲取任務(wù)的以及如何實(shí)現(xiàn)超時(shí)的
上一節(jié)我們說(shuō)到,線程在執(zhí)行完任務(wù)之后,會(huì)繼續(xù)從getTask方法中獲取任務(wù),獲取不到就會(huì)退出。接下來(lái)我們就來(lái)看一看getTask方法的實(shí)現(xiàn)。
getTask方法
getTask方法,前面就是線程池的一些狀態(tài)的判斷,這里有一行代碼
?
?
?
?
這行代碼是判斷,當(dāng)前過(guò)來(lái)獲取任務(wù)的線程是否可以超時(shí)退出。如果allowCoreThreadTimeOut設(shè)置為true或者線程池當(dāng)前的線程數(shù)大于核心線程數(shù),也就是corePoolSize,那么該獲取任務(wù)的線程就可以超時(shí)退出。
那是怎么做到超時(shí)退出呢,就是這行核心代碼
?
?
?
?
會(huì)根據(jù)是否允許超時(shí)來(lái)選擇調(diào)用阻塞隊(duì)列workQueue的poll方法或者take方法。如果允許超時(shí),則會(huì)調(diào)用poll方法,傳入keepAliveTime,也就是構(gòu)造線程池時(shí)傳入的空閑時(shí)間,這個(gè)方法的意思就是從隊(duì)列中阻塞keepAliveTime時(shí)間來(lái)獲取任務(wù),獲取不到就會(huì)返回null;如果不允許超時(shí),就會(huì)調(diào)用take方法,這個(gè)方法會(huì)一直阻塞獲取任務(wù),直到從隊(duì)列中獲取到任務(wù)位置。從這里可以看到keepAliveTime是如何使用的了。
所以到這里應(yīng)該就知道線程池中的線程為什么可以做到空閑一定時(shí)間就退出了吧。其實(shí)最主要的是利用了阻塞隊(duì)列的poll方法的實(shí)現(xiàn),這個(gè)方法可以指定超時(shí)時(shí)間,一旦線程達(dá)到了keepAliveTime還沒(méi)有獲取到任務(wù),那么就會(huì)返回null,上一小節(jié)提到,getTask方法返回null,線程就會(huì)退出。
這里也有一個(gè)細(xì)節(jié),就是判斷當(dāng)前獲取任務(wù)的線程是否可以超時(shí)退出的時(shí)候,如果將allowCoreThreadTimeOut設(shè)置為true,那么所有線程走到這個(gè)timed都是true,那么所有的線程,包括核心線程都可以做到超時(shí)退出。如果你的線程池需要將核心線程超時(shí)退出,那么可以通過(guò)allowCoreThreadTimeOut方法將allowCoreThreadTimeOut變量設(shè)置為true。
整個(gè)getTask方法以及線程超時(shí)退出的機(jī)制如圖所示
六、線程池的5種狀態(tài)
線程池內(nèi)部有5個(gè)常量來(lái)代表線程池的五種狀態(tài)
RUNNING:線程池創(chuàng)建時(shí)就是這個(gè)狀態(tài),能夠接收新任務(wù),以及對(duì)已添加的任務(wù)進(jìn)行處理。
SHUTDOWN:調(diào)用shutdown方法線程池就會(huì)轉(zhuǎn)換成SHUTDOWN狀態(tài),此時(shí)線程池不再接收新任務(wù),但能繼續(xù)處理已添加的任務(wù)到隊(duì)列中任務(wù)。
STOP:調(diào)用shutdownNow方法線程池就會(huì)轉(zhuǎn)換成STOP狀態(tài),不接收新任務(wù),也不能繼續(xù)處理已添加的任務(wù)到隊(duì)列中任務(wù),并且會(huì)嘗試中斷正在處理的任務(wù)的線程。
TIDYING:SHUTDOWN 狀態(tài)下,任務(wù)數(shù)為 0, 其他所有任務(wù)已終止,線程池會(huì)變?yōu)?TIDYING 狀態(tài)。線程池在 SHUTDOWN 狀態(tài),任務(wù)隊(duì)列為空且執(zhí)行中任務(wù)為空,線程池會(huì)變?yōu)?TIDYING 狀態(tài)。線程池在 STOP 狀態(tài),線程池中執(zhí)行中任務(wù)為空時(shí),線程池會(huì)變?yōu)?TIDYING 狀態(tài)。
TERMINATED:線程池徹底終止。線程池在 TIDYING 狀態(tài)執(zhí)行完 terminated() 方法就會(huì)轉(zhuǎn)變?yōu)?TERMINATED 狀態(tài)。
線程池狀態(tài)具體是存在ctl成員變量中,ctl中不僅存儲(chǔ)了線程池的狀態(tài)還存儲(chǔ)了當(dāng)前線程池中線程數(shù)的大小
?
?
?
?
最后畫(huà)個(gè)圖來(lái)總結(jié)一下這5種狀態(tài)的流轉(zhuǎn)
其實(shí),在線程池運(yùn)行過(guò)程中,絕大多數(shù)操作執(zhí)行前都得判斷當(dāng)前線程池處于哪種狀態(tài),再來(lái)決定是否繼續(xù)執(zhí)行該操作。
七、線程池的關(guān)閉
線程池提供了shutdown和shutdownNow兩個(gè)方法來(lái)關(guān)閉線程池。
shutdown方法
就是將線程池的狀態(tài)修改為SHUTDOWN,然后嘗試打斷空閑的線程(如何判斷空閑,上面在說(shuō)Worker繼承AQS的時(shí)候說(shuō)過(guò)),也就是在阻塞等待任務(wù)的線程。
shutdownNow方法
就是將線程池的狀態(tài)修改為STOP,然后嘗試打斷所有的線程,從阻塞隊(duì)列中移除剩余的任務(wù),這也是為什么shutdownNow不能執(zhí)行剩余任務(wù)的原因。
所以也可以看出shutdown方法和shutdownNow方法的主要區(qū)別就是,shutdown之后還能處理在隊(duì)列中的任務(wù),shutdownNow直接就將任務(wù)從隊(duì)列中移除,線程池里的線程就不再處理了。
八、線程池的監(jiān)控
在項(xiàng)目中使用線程池的時(shí)候,一般需要對(duì)線程池進(jìn)行監(jiān)控,方便出問(wèn)題的時(shí)候進(jìn)行查看。線程池本身提供了一些方法來(lái)獲取線程池的運(yùn)行狀態(tài)。
getCompletedTaskCount:已經(jīng)執(zhí)行完成的任務(wù)數(shù)量
getLargestPoolSize:線程池里曾經(jīng)創(chuàng)建過(guò)的最大的線程數(shù)量。這個(gè)主要是用來(lái)判斷線程是否滿過(guò)。
getActiveCount:獲取正在執(zhí)行任務(wù)的線程數(shù)據(jù)
getPoolSize:獲取當(dāng)前線程池中線程數(shù)量的大小
除了線程池提供的上述已經(jīng)實(shí)現(xiàn)的方法,同時(shí)線程池也預(yù)留了很多擴(kuò)展方法。比如在runWorker方法里面,在執(zhí)行任務(wù)之前會(huì)回調(diào)beforeExecute方法,執(zhí)行任務(wù)之后會(huì)回調(diào)afterExecute方法,而這些方法默認(rèn)都是空實(shí)現(xiàn),你可以自己繼承ThreadPoolExecutor來(lái)擴(kuò)展重寫(xiě)這些方法,來(lái)實(shí)現(xiàn)自己想要的功能。
九、Executors構(gòu)建線程池以及問(wèn)題分析
JDK內(nèi)部提供了Executors這個(gè)工具類,來(lái)快速的創(chuàng)建線程池。
固定線程數(shù)量的線程池:核心線程數(shù)與最大線程數(shù)相等
單個(gè)線程數(shù)量的線程池
接近無(wú)限大線程數(shù)量的線程池
帶定時(shí)調(diào)度功能的線程池
雖然JDK提供了快速創(chuàng)建線程池的方法,但是其實(shí)不推薦使用Executors來(lái)創(chuàng)建線程池,因?yàn)閺纳厦鏄?gòu)造線程池可以看出,newFixedThreadPool線程池,由于使用了LinkedBlockingQueue,隊(duì)列的容量默認(rèn)是無(wú)限大,實(shí)際使用中出現(xiàn)任務(wù)過(guò)多時(shí)會(huì)導(dǎo)致內(nèi)存溢出;newCachedThreadPool線程池由于核心線程數(shù)無(wú)限大,當(dāng)任務(wù)過(guò)多的時(shí)候,會(huì)導(dǎo)致創(chuàng)建大量的線程,可能機(jī)器負(fù)載過(guò)高,可能會(huì)導(dǎo)致服務(wù)宕機(jī)。
十、線程池的使用場(chǎng)景
在java程序中,其實(shí)經(jīng)常需要用到多線程來(lái)處理一些業(yè)務(wù),但是不建議單純使用繼承Thread或者實(shí)現(xiàn)Runnable接口的方式來(lái)創(chuàng)建線程,那樣就會(huì)導(dǎo)致頻繁創(chuàng)建及銷毀線程,同時(shí)創(chuàng)建過(guò)多的線程也可能引發(fā)資源耗盡的風(fēng)險(xiǎn)。所以在這種情況下,使用線程池是一種更合理的選擇,方便管理任務(wù),實(shí)現(xiàn)了線程的重復(fù)利用。所以線程池一般適合那種需要異步或者多線程處理任務(wù)的場(chǎng)景。
十一、實(shí)際項(xiàng)目中如何合理的自定義線程池
通過(guò)上面分析提到,通過(guò)Executors這個(gè)工具類來(lái)創(chuàng)建的線程池其實(shí)都無(wú)法滿足實(shí)際的使用場(chǎng)景,那么在實(shí)際的項(xiàng)目中,到底該如何構(gòu)造線程池呢,該如何合理的設(shè)置參數(shù)?
1)線程數(shù)
線程數(shù)的設(shè)置主要取決于業(yè)務(wù)是IO密集型還是CPU密集型。
CPU密集型指的是任務(wù)主要使用來(lái)進(jìn)行大量的計(jì)算,沒(méi)有什么導(dǎo)致線程阻塞。一般這種場(chǎng)景的線程數(shù)設(shè)置為CPU核心數(shù)+1。
IO密集型:當(dāng)執(zhí)行任務(wù)需要大量的io,比如磁盤(pán)io,網(wǎng)絡(luò)io,可能會(huì)存在大量的阻塞,所以在IO密集型任務(wù)中使用多線程可以大大地加速任務(wù)的處理。一般線程數(shù)設(shè)置為 2*CPU核心數(shù)
java中用來(lái)獲取CPU核心數(shù)的方法是:
?
?
?
?
2)線程工廠
一般建議自定義線程工廠,構(gòu)建線程的時(shí)候設(shè)置線程的名稱,這樣就在查日志的時(shí)候就方便知道是哪個(gè)線程執(zhí)行的代碼。
3)有界隊(duì)列
一般需要設(shè)置有界隊(duì)列的大小,比如LinkedBlockingQueue在構(gòu)造的時(shí)候就可以傳入?yún)?shù),來(lái)限制隊(duì)列中任務(wù)數(shù)據(jù)的大小,這樣就不會(huì)因?yàn)闊o(wú)限往隊(duì)列中扔任務(wù)導(dǎo)致系統(tǒng)的oom。
編輯:黃飛
?
boolean?timed?=?allowCoreThreadTimeOut?||?wc?>?corePoolSize;
Runnable?r?=?timed??
????????????????????workQueue.poll(keepAliveTime,?TimeUnit.NANOSECONDS)?:
????????????????????workQueue.take();
private?final?AtomicInteger?ctl?=?new?AtomicInteger(ctlOf(RUNNING,?0));
Runtime.getRuntime().availableProcessors();
評(píng)論
查看更多