簡單剖析Java中動態(tài)線程池的擴容以及縮容操作
前言
在項目中,我們經(jīng)常會使用到線程來處理加快我們的任務。但為了節(jié)約資源,大多數(shù)程序員都會把線程進行池化,使用線程池來更好的支持我們的業(yè)務。
Java線程池ThreadPoolExecutor
有幾個比較核心的參數(shù),如corePoolSize、maximumPoolSize
等等。無論是在工作中還是在面試中,都會被問到,如何正確的設置這幾個參數(shù)。
線程池的參數(shù)并不好配置。一方面線程池的運行機制不是很好理解,配置合理需要強依賴開發(fā)人員的個人經(jīng)驗和知識。項目IO密集型還是CPU密集型等等,總歸很難確定一個完美的參數(shù),此時就有了動態(tài)線程池的誕生。
動態(tài)線程池(DTP)原理
其實動態(tài)線程池并不是很高大上的技術,它底層依舊是依賴了ThreadPoolExecutor
的一些核心接口方法。我們通過下面圖片可以很清楚的看到,ThreadPoolExecutor
本身就給我們提供了很多鉤子方法,讓我們?nèi)ザㄖ苹?/p>
那么其原理也非常簡單了,我們在運行中假設有一個線程池叫做TaskExecutor
- 他的核心線程池默認假設是10,現(xiàn)在我發(fā)覺不夠用了,此時我想把他的核心線程池調(diào)整為20
- 我可以寫一個遠程配置(可以阿波羅,zk,redis什么都可以)。然后監(jiān)聽到了這個配置變?yōu)榱薱ore.pool.size=20
- 然后我獲取到了這個線程池
TaskExecutor
,并且調(diào)用setCorePoolSize(20)
,那么這個TaskExecutor
核心線程數(shù)就變?yōu)榱?0
就是這么簡單,撥開表面,探究原理,內(nèi)部其實非常的簡單。當時公司里面的線程池還有加一些友好的界面、監(jiān)控告警、操作日志、權限校驗、審核等等,但本質(zhì)就是監(jiān)聽配置,然后調(diào)用setCorePoolSize方法去實現(xiàn)的,最大線程數(shù)類似。
public void setCorePoolSize(int corePoolSize) { if (corePoolSize < 0) throw new IllegalArgumentException(); int delta = corePoolSize - this.corePoolSize; this.corePoolSize = corePoolSize; if (workerCountOf(ctl.get()) > corePoolSize) interruptIdleWorkers(); else if (delta > 0) { int k = Math.min(delta, workQueue.size()); while (k-- > 0 && addWorker(null, true)) { if (workQueue.isEmpty()) break; } } }
動態(tài)線程池縮容
首先提出幾個問題
- 核心線程數(shù)為5,現(xiàn)在有3個線程在執(zhí)行,并且沒有執(zhí)行完畢,我修改核心線程數(shù)為4,是否修改成功
- 核心線程數(shù)為5,現(xiàn)在有3個線程在執(zhí)行,并且沒有執(zhí)行完畢,我修改核心線程數(shù)為1,是否修改成功
讓我們帶著疑問去思考問題。
- 首先第一個問題,因為核心線程池數(shù)為5,僅有3個在執(zhí)行,我修改為4,那么因為有2個空閑的線程,它只需要銷毀1個空閑線程即可,因此是成功的
- 第二個問題,核心線程池數(shù)為5,僅有3個在執(zhí)行,我修改為1。雖然有2個空閑線程,但是我需要銷毀4個線程。因為有2個空閑線程,2個非空閑線程。我只能銷毀2個空閑線程,另外2個執(zhí)行的任務不能被打斷,也就是執(zhí)行后仍然為3個核心線程數(shù)。
- 那什么時候銷毀剩下2個執(zhí)行的線程呢,等到2個執(zhí)行的任務完畢之后,就會銷毀它了。假設這個任務是一個死循環(huán),永遠不會結束,那么核心線程數(shù)永遠是3,永遠不能設置為1
我們舉一個代碼的例子如下
ThreadPoolExecutor es = new ThreadPoolExecutor(5, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>()); es.prestartAllCoreThreads(); // 預啟動所有核心線程 // 啟動三個任務,執(zhí)行次數(shù)不一樣 for (int i = 0; i < 3; i++) { int finalI = i; es.execute(() -> { int cnt = 0; while (true) { try { cnt++; TimeUnit.SECONDS.sleep(2); if (cnt > finalI + 1) { log.info(Thread.currentThread().getName() + " 執(zhí)行完畢"); break; } } catch (InterruptedException e) { e.printStackTrace(); } } }); } TimeUnit.SECONDS.sleep(1); // 等待線程池中的線程執(zhí)行 log.info("修改前 es = {}", es); // 這里核心線程數(shù)必定是5 es.setCorePoolSize(1); // 修改核心線程數(shù)為1,但是核心線程數(shù)為5,并且有3個線程在執(zhí)行任務, while (true) { TimeUnit.SECONDS.sleep(1); // 等待 log.info("修改后 es = {}", es); }
輸出結果為如下
// 修改前核心線程數(shù)為5,運行線程數(shù)為3 [修改前 es = java.util.concurrent.ThreadPoolExecutor@72d818d1[Running, pool size = 5, active threads = 3, queued tasks = 0, completed tasks = 0]] // 因為有2個空閑線程,先把2個空閑線程給銷毀了,剩下3個線程 [修改后 es = java.util.concurrent.ThreadPoolExecutor@72d818d1[Running, pool size = 3, active threads = 3, queued tasks = 0, completed tasks = 0]] // 等第1個任務執(zhí)行完畢,剩下2個線程 [Main.lambda$d$0:38] [pool-2-thread-1 執(zhí)行完畢] [修改后 es = java.util.concurrent.ThreadPoolExecutor@72d818d1[Running, pool size = 2, active threads = 2, queued tasks = 0, completed tasks = 1]] // 等第2個任務執(zhí)行完畢,剩下1個線程 [Main.lambda$d$0:38] [pool-2-thread-2 執(zhí)行完畢] [修改后 es = java.util.concurrent.ThreadPoolExecutor@72d818d1[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 2]] // 等第3個任務執(zhí)行完畢,剩下1個線程。因為我修改的就是1個核心線程 [Main.lambda$d$0:38] [pool-2-thread-3 執(zhí)行完畢] [修改后 es = java.util.concurrent.ThreadPoolExecutor@72d818d1[Running, pool size = 1, active threads = 0, queued tasks = 0, completed tasks = 3]]
有興趣的讀者可以拿這塊帶去自己去試試,輸出結果里面的注釋 我寫的非常詳細,大家可以詳細品品這塊輸出結果。
動態(tài)線程池擴容
擴容我就不提問問題了,和縮容異曲同工,但我希望讀者可以先看下以下代碼,不要看答案,認為會輸出什么結果,看下是否和自己想的是否一樣,如果一樣,那說明你已經(jīng)完全懂了,如果不一樣,是什么原因。
// 核心線程數(shù)1,最大線程數(shù)10 ThreadPoolExecutor es = new ThreadPoolExecutor(1, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>()); es.prestartAllCoreThreads(); // 預啟動所有核心線程 for (int i = 0; i < 5; i++) { int finalI = i; es.execute(() -> { int cnt = 0; while (true) { try { cnt++; TimeUnit.SECONDS.sleep(2); if (cnt > finalI + 1) { log.info(Thread.currentThread().getName() + " 執(zhí)行完畢"); break; } } catch (InterruptedException e) { e.printStackTrace(); } } }); } TimeUnit.SECONDS.sleep(1); // 等待線程池中的線程執(zhí)行 log.info("修改前 es = {}", es); // 這里核心線程數(shù)必定是1, 隊列里面有4個任務 es.setCorePoolSize(3); // 修改核心線程數(shù)為3 while (true) { TimeUnit.SECONDS.sleep(1); // 等待 log.info("修改后 es = {}", es); }
輸出結果為如下 (注意觀察輸出queued tasks的變化?。?!)
[修改前 es = java.util.concurrent.ThreadPoolExecutor@1e397ed7[Running, pool size = 1, active threads = 1, queued tasks = 4, completed tasks = 0]]
[修改后 es = java.util.concurrent.ThreadPoolExecutor@1e397ed7[Running, pool size = 3, active threads = 3, queued tasks = 2, completed tasks = 0]]
[Main.lambda$a$1:73] [pool-2-thread-1 執(zhí)行完畢]
[修改后 es = java.util.concurrent.ThreadPoolExecutor@1e397ed7[Running, pool size = 3, active threads = 3, queued tasks = 1, completed tasks = 1]]
[Main.lambda$a$1:73] [pool-2-thread-2 執(zhí)行完畢]
[修改后 es = java.util.concurrent.ThreadPoolExecutor@1e397ed7[Running, pool size = 3, active threads = 3, queued tasks = 0, completed tasks = 2]]
[Main.lambda$a$1:73] [pool-2-thread-3 執(zhí)行完畢]
[修改后 es = java.util.concurrent.ThreadPoolExecutor@1e397ed7[Running, pool size = 3, active threads = 2, queued tasks = 0, completed tasks = 3]]
[Main.lambda$a$1:73] [pool-2-thread-1 執(zhí)行完畢]
[修改后 es = java.util.concurrent.ThreadPoolExecutor@1e397ed7[Running, pool size = 3, active threads = 1, queued tasks = 0, completed tasks = 4]]
[Main.lambda$a$1:73] [pool-2-thread-2 執(zhí)行完畢]
[修改后 es = java.util.concurrent.ThreadPoolExecutor@1e397ed7[Running, pool size = 3, active threads = 0, queued tasks = 0, completed tasks = 5]]
最后
在業(yè)務中,我們?yōu)榱颂岣咝适褂昧司€程,為了加快線程我們使用了線程池,而又為了更好的利用線程池的資源,我們又實現(xiàn)了動態(tài)化線程池。這也就是遇到問題、探索問題、解決問題的一套思路吧。
我們從底層原理分析,發(fā)現(xiàn)動態(tài)線程池的底層原理非常簡單,希望大家不要恐懼,往往撥開外衣,發(fā)現(xiàn)里面最根本的原理,才能是我們更好的捋清楚其中的邏輯。
到此這篇關于簡單剖析Java中動態(tài)線程池的擴容以及縮容操作的文章就介紹到這了,更多相關Java動態(tài)線程池內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
springboot+WebMagic+MyBatis爬蟲框架的使用
本文是對spring boot+WebMagic+MyBatis做了整合,使用WebMagic爬取數(shù)據(jù),然后通過MyBatis持久化爬取的數(shù)據(jù)到mysql數(shù)據(jù)庫。具有一定的參考價值,感興趣的可以了解一下2021-08-08Java中的字符型文件流FileReader和FileWriter詳細解讀
這篇文章主要介紹了Java中的字符型文件流FileReader和FileWriter詳細解讀,與字節(jié)型文件流不同,字節(jié)型文件流讀取和寫入的都是一個又一個的字節(jié),而字符型文件流操作的單位是一個又一個的字符,字符型流認為一個字母是一個字符,而一個漢字也是一個字符,需要的朋友可以參考下2023-10-10