程序運行柳恐,其本質(zhì)上伐脖,是對系統(tǒng)資源(CPU热幔、內(nèi)存、磁盤讼庇、網(wǎng)絡(luò)等等)的使用绎巨。如何高效的使用這些資源是我們編程優(yōu)化演進(jìn)的一個方向。今天說的線程池是對CPU的利用的優(yōu)化手段蠕啄。
網(wǎng)上有不少介紹如何使用線程池的文章场勤,那我想說點什么呢?我希望查看線程池原理歼跟,明白池化技術(shù)的基本設(shè)計思路和媳。遇到其他相似問題可以解決。
池化技術(shù)
何為池化技術(shù)嘹承,簡單點來說窗价,就是提前保存大量的資源,以備不時之需叹卷。在資源有限的情況下撼港,該技術(shù)可以大大提升資源的利用率,提升性能等骤竹。
目前比較典型的池化技術(shù)有:
線程池帝牡、連接池、內(nèi)存池蒙揣、對象池等靶溜。
本文主要來介紹一下其中比較簡單的線程池的實現(xiàn)原理,希望讀者們可以舉一反三懒震,通過對線程池的理解罩息,學(xué)習(xí)并掌握所有的編程中池化技術(shù)的底層原理,一通百通个扰。
創(chuàng)建一個線程
在java的并發(fā)編程中瓷炮,線程是十分重要的,在Java中递宅,創(chuàng)建一個線程比較簡單:
public class App {
public static void main(String[] args) throws Exception {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("線程運行中");
}
}).start();
}
}
我們通過創(chuàng)建一個線程對象娘香,并且實現(xiàn)Runnable接口就可以實現(xiàn)一個簡單的線程“炝洌可以利用上多核CPU烘绽。當(dāng)一個任務(wù)結(jié)束,當(dāng)前線程就結(jié)束俐填。
但很多時候安接,我們不止會執(zhí)行一個任務(wù)。如果每次都是如此的創(chuàng)建線程->執(zhí)行任務(wù)->銷毀線程英融,會造成很大的性能開銷的盏檐。
那能否一個線程創(chuàng)建后呀打,執(zhí)行完一個任務(wù)后,又去執(zhí)行另一個任務(wù)糯笙,而不是銷毀贬丛。這就是線程池。
這就是池化技術(shù)的思想给涕,通過預(yù)先創(chuàng)建好多個線程豺憔,放在池中,這樣可以在需要使用線程的時候直接獲取够庙,避免多次重復(fù)創(chuàng)建恭应、銷毀代理的開銷。
線程池的簡單使用
以下代碼耘眨,就是在java中創(chuàng)建線程池:
import java.util.concurrent.*;
public class App {
public static void main(String[] args) throws Exception {
ExecutorService executorService = new ThreadPoolExecutor(1, 1,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10));
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("abcdefg");
}
});
executorService.shutdown();
}
}
JDK提供給外部的接口也很簡單昼榛。直接調(diào)用ThreadPoolExecutor構(gòu)造一個就可以了,也可以通過Executors靜態(tài)工廠構(gòu)建剔难,但一般不建議胆屿。
可以看到,開發(fā)者想要在代碼中使用線程池還是比較簡單的偶宫,這得益于Java給我們封裝好了一系列的API非迹。很多時候,我們需要知道這些API后面干了些啥纯趋,以便于我們更好的設(shè)計與實現(xiàn)我們的代碼憎兽。
線程池構(gòu)造函數(shù)
通常,一般構(gòu)造函數(shù)會反映出這個工具或這個對象的數(shù)據(jù)存儲結(jié)構(gòu)吵冒。
如果把線程池比作一個公司纯命。公司會有正式員工處理正常業(yè)務(wù),如果工作量大的話痹栖,會雇傭外包人員來工作亿汞。閑時就可以釋放外包人員以減少公司管理開銷。一個公司因為成本關(guān)系结耀,雇傭的人員始終是有最大數(shù)留夜。如果這時候還有任務(wù)處理不過來匙铡,就走需求池排任務(wù)图甜。
- acc : 獲取調(diào)用上下文
- corePoolSize: 核心線程數(shù)量,可以類比正式員工數(shù)量鳖眼,常駐線程數(shù)量黑毅。
- maximumPoolSize: 最大的線程數(shù)量,公司最多雇傭員工數(shù)量钦讳。常駐+臨時線程數(shù)量矿瘦。
- workQueue:多余任務(wù)等待隊列枕面,再多的人都處理不過來了,需要等著缚去,在這個地方等潮秘。
- keepAliveTime:非核心線程空閑時間,就是外包人員等了多久易结,如果還沒有活干枕荞,解雇了。
- threadFactory: 創(chuàng)建線程的工廠搞动,在這個地方可以統(tǒng)一處理創(chuàng)建的線程的屬性躏精。每個公司對員工的要求不一樣,恩鹦肿,在這里設(shè)置員工的屬性矗烛。
- handler:線程池拒絕策略,什么意思呢箩溃?就是當(dāng)任務(wù)實在是太多瞭吃,人也不夠,需求池也排滿了涣旨,還有任務(wù)咋辦虱而?默認(rèn)是不處理,拋出異常告訴任務(wù)提交者开泽,我這忙不過來了牡拇。
添加一個任務(wù)
核心模塊用紅框標(biāo)記了。
- 第一個紅框:判斷工作的線程數(shù)有沒有超過最大核心線程數(shù)量穆律,如果沒有超過新鎮(zhèn)新的worker線程惠呼。并且推出。
- 第二個紅框:判斷線程池是否在運行峦耘,如果在剔蹋,任務(wù)隊列是否允許插入,插入成功再次驗證線程池是否運行辅髓,如果不再運行泣崩,移除插入的任務(wù),然后拋出拒絕策略洛口。如果再運行矫付,沒有線程了,就啟用一個線程第焰。
- 第三個紅框:如果添加非核心線程失敗买优,就直接拒絕了。
這里邏輯稍微有點復(fù)雜,畫了個流程圖僅供參考
接下來杀赢,我們看看如何添加一個工作線程的烘跺? addWork
添加worker線程
這里代碼有點長,沒關(guān)系脂崔,也是分塊的滤淳,總共有5個關(guān)鍵的代碼塊。
- 第一個紅框:做是否能夠添加工作線程條件過濾砌左。
- 判斷線程池狀態(tài)娇钱,如果線程池已經(jīng)關(guān)了,就不提交任務(wù)了绊困。
-
第二個紅框:做自旋文搂,更新創(chuàng)建線程數(shù)量。
- 第一個紅框:獲取線程池主鎖秤朗。
- 第二個紅框:添加線程到workers中(線程池中)煤蹭。
- 第三個紅框:啟動新建的線程。
這樣就應(yīng)該清晰很多了取视。
有人或許會疑問 retry 是什么硝皂?這個是java中的goto語法。只能運用在break和continue后面作谭。
接下來稽物,我們看看works是什么。
一個hashSet折欠。
到這來就完成了一個任務(wù)的提交贝或。當(dāng)一個線程完成了首次任務(wù)的執(zhí)行,后續(xù)如何處理其他的請求的呢锐秦?
worker線程處理隊列任務(wù)
- 第一個紅框:是否是第一次執(zhí)行任務(wù)咪奖,或者從隊列中可以獲取到任務(wù)。
- 第二個紅框:獲取到任務(wù)后酱床,執(zhí)行任務(wù)開始前操作鉤子羊赵。
- 第三個紅框:執(zhí)行任務(wù)。
- 第四個紅框:執(zhí)行任務(wù)后鉤子扇谣。
這兩個鉤子(beforeExecute昧捷,afterExecute)允許我們自己繼承線程池,做任務(wù)執(zhí)行前后處理罐寨。有意思靡挥。
到這里,源代碼分析到此為止衩茸。接下來做一下簡單的總結(jié)芹血。
總結(jié)
- 所謂線程池本質(zhì)是一個hashSet。多余的任務(wù)會放在阻塞隊列中楞慈。
- 只有當(dāng)阻塞隊列滿了后幔烛,才會觸發(fā)非核心線程的創(chuàng)建。所以非核心線程只是臨時過來打雜的囊蓝。直到空閑了饿悬,然后自己關(guān)閉了。
- 線程池提供了兩個鉤子(beforeExecute聚霜,afterExecute)給我們狡恬,我們繼承線程池,在執(zhí)行任務(wù)前后做一些事情蝎宇。
- 線程池原理關(guān)鍵技術(shù):鎖(lock,cas)弟劲、阻塞隊列、hashSet(資源池)
最后希望對你理解線程池有幫助姥芥。
都看到這里了兔乞,成神之路上,要不要一起凉唐?