原創(chuàng)文章&經(jīng)驗(yàn)總結(jié)&從校招到A廠一路陽(yáng)光一路滄桑
詳情請(qǐng)戳www.codercc.com
1. 為什么要使用線程池
在實(shí)際使用中蚜厉,線程是很占用系統(tǒng)資源的贝攒,如果對(duì)線程管理不善很容易導(dǎo)致系統(tǒng)問(wèn)題。因此臼隔,在大多數(shù)并發(fā)框架中都會(huì)使用線程池來(lái)管理線程遂唧,使用線程池管理線程主要有如下好處:
- 降低資源消耗如输。通過(guò)復(fù)用已存在的線程和降低線程關(guān)閉的次數(shù)來(lái)盡可能降低系統(tǒng)性能損耗有鹿;
- 提升系統(tǒng)響應(yīng)速度旭旭。通過(guò)復(fù)用線程,省去創(chuàng)建線程的過(guò)程葱跋,因此整體上提升了系統(tǒng)的響應(yīng)速度持寄;
- 提高線程的可管理性。線程是稀缺資源娱俺,如果無(wú)限制的創(chuàng)建际看,不僅會(huì)消耗系統(tǒng)資源,還會(huì)降低系統(tǒng)的穩(wěn)定性矢否,因此仲闽,需要使用線程池來(lái)管理線程。
2. 線程池的工作原理
當(dāng)一個(gè)并發(fā)任務(wù)提交給線程池僵朗,線程池分配線程去執(zhí)行任務(wù)的過(guò)程如下圖所示:
從圖可以看出赖欣,線程池執(zhí)行所提交的任務(wù)過(guò)程主要有這樣幾個(gè)階段:
- 先判斷線程池中核心線程池所有的線程是否都在執(zhí)行任務(wù)。如果不是验庙,則新創(chuàng)建一個(gè)線程執(zhí)行剛提交的任務(wù)顶吮,否則,核心線程池中所有的線程都在執(zhí)行任務(wù)粪薛,則進(jìn)入第2步悴了;
- 判斷當(dāng)前阻塞隊(duì)列是否已滿,如果未滿违寿,則將提交的任務(wù)放置在阻塞隊(duì)列中湃交;否則,則進(jìn)入第3步藤巢;
- 判斷線程池中所有的線程是否都在執(zhí)行任務(wù)搞莺,如果沒(méi)有,則創(chuàng)建一個(gè)新的線程來(lái)執(zhí)行任務(wù)掂咒,否則才沧,則交給飽和策略進(jìn)行處理
3. 線程池的創(chuàng)建
創(chuàng)建線程池主要是ThreadPoolExecutor類來(lái)完成,ThreadPoolExecutor的有許多重載的構(gòu)造方法绍刮,通過(guò)參數(shù)最多的構(gòu)造方法來(lái)理解創(chuàng)建線程池有哪些需要配置的參數(shù)温圆。ThreadPoolExecutor的構(gòu)造方法為:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
下面對(duì)參數(shù)進(jìn)行說(shuō)明:
- corePoolSize:表示核心線程池的大小。當(dāng)提交一個(gè)任務(wù)時(shí)孩革,如果當(dāng)前核心線程池的線程個(gè)數(shù)沒(méi)有達(dá)到corePoolSize岁歉,則會(huì)創(chuàng)建新的線程來(lái)執(zhí)行所提交的任務(wù),即使當(dāng)前核心線程池有空閑的線程嫉戚。如果當(dāng)前核心線程池的線程個(gè)數(shù)已經(jīng)達(dá)到了corePoolSize刨裆,則不再重新創(chuàng)建線程澈圈。如果調(diào)用了
prestartCoreThread()
或者prestartAllCoreThreads()
彬檀,線程池創(chuàng)建的時(shí)候所有的核心線程都會(huì)被創(chuàng)建并且啟動(dòng)帆啃。 - maximumPoolSize:表示線程池能創(chuàng)建線程的最大個(gè)數(shù)。如果當(dāng)阻塞隊(duì)列已滿時(shí)窍帝,并且當(dāng)前線程池線程個(gè)數(shù)沒(méi)有超過(guò)maximumPoolSize的話努潘,就會(huì)創(chuàng)建新的線程來(lái)執(zhí)行任務(wù)。
- keepAliveTime:空閑線程存活時(shí)間坤学。如果當(dāng)前線程池的線程個(gè)數(shù)已經(jīng)超過(guò)了corePoolSize疯坤,并且線程空閑時(shí)間超過(guò)了keepAliveTime的話,就會(huì)將這些空閑線程銷毀深浮,這樣可以盡可能降低系統(tǒng)資源消耗压怠。
- unit:時(shí)間單位。為keepAliveTime指定時(shí)間單位飞苇。
- workQueue:阻塞隊(duì)列菌瘫。用于保存任務(wù)的阻塞隊(duì)列,關(guān)于阻塞隊(duì)列可以看這篇文章布卡∮耆茫可以使用ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue。
- threadFactory:創(chuàng)建線程的工程類忿等∑苤遥可以通過(guò)指定線程工廠為每個(gè)創(chuàng)建出來(lái)的線程設(shè)置更有意義的名字,如果出現(xiàn)并發(fā)問(wèn)題贸街,也方便查找問(wèn)題原因庵寞。
- handler:飽和策略。當(dāng)線程池的阻塞隊(duì)列已滿和指定的線程都已經(jīng)開(kāi)啟薛匪,說(shuō)明當(dāng)前線程池已經(jīng)處于飽和狀態(tài)了皇帮,那么就需要采用一種策略來(lái)處理這種情況。采用的策略有這幾種:
- AbortPolicy: 直接拒絕所提交的任務(wù)蛋辈,并拋出RejectedExecutionException異常属拾;
- CallerRunsPolicy:只用調(diào)用者所在的線程來(lái)執(zhí)行任務(wù);
- DiscardPolicy:不處理直接丟棄掉任務(wù)冷溶;
- DiscardOldestPolicy:丟棄掉阻塞隊(duì)列中存放時(shí)間最久的任務(wù)渐白,執(zhí)行當(dāng)前任務(wù)
線程池執(zhí)行邏輯
通過(guò)ThreadPoolExecutor創(chuàng)建線程池后,提交任務(wù)后執(zhí)行過(guò)程是怎樣的逞频,下面來(lái)通過(guò)源碼來(lái)看一看纯衍。execute方法源碼如下:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
//如果線程池的線程個(gè)數(shù)少于corePoolSize則創(chuàng)建新線程執(zhí)行當(dāng)前任務(wù)
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果線程個(gè)數(shù)大于corePoolSize或者創(chuàng)建線程失敗,則將任務(wù)存放在阻塞隊(duì)列workQueue中
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//如果當(dāng)前任務(wù)無(wú)法放進(jìn)阻塞隊(duì)列中苗胀,則創(chuàng)建新的線程來(lái)執(zhí)行任務(wù)
else if (!addWorker(command, false))
reject(command);
}
ThreadPoolExecutor的execute方法執(zhí)行邏輯請(qǐng)見(jiàn)注釋襟诸。下圖為T(mén)hreadPoolExecutor的execute方法的執(zhí)行示意圖:
execute方法執(zhí)行邏輯有這樣幾種情況:
- 如果當(dāng)前運(yùn)行的線程少于corePoolSize瓦堵,則會(huì)創(chuàng)建新的線程來(lái)執(zhí)行新的任務(wù);
- 如果運(yùn)行的線程個(gè)數(shù)等于或者大于corePoolSize歌亲,則會(huì)將提交的任務(wù)存放到阻塞隊(duì)列workQueue中菇用;
- 如果當(dāng)前workQueue隊(duì)列已滿的話,則會(huì)創(chuàng)建新的線程來(lái)執(zhí)行任務(wù)陷揪;
- 如果線程個(gè)數(shù)已經(jīng)超過(guò)了maximumPoolSize惋鸥,則會(huì)使用飽和策略RejectedExecutionHandler來(lái)進(jìn)行處理。
需要注意的是悍缠,線程池的設(shè)計(jì)思想就是使用了核心線程池corePoolSize卦绣,阻塞隊(duì)列workQueue和線程池maximumPoolSize,這樣的緩存策略來(lái)處理任務(wù)飞蚓,實(shí)際上這樣的設(shè)計(jì)思想在需要框架中都會(huì)使用滤港。
4. 線程池的關(guān)閉
關(guān)閉線程池,可以通過(guò)shutdown
和shutdownNow
這兩個(gè)方法趴拧。它們的原理都是遍歷線程池中所有的線程溅漾,然后依次中斷線程。shutdown
和shutdownNow
還是有不一樣的地方:
-
shutdownNow
首先將線程池的狀態(tài)設(shè)置為STOP,然后嘗試停止所有的正在執(zhí)行和未執(zhí)行任務(wù)的線程八堡,并返回等待執(zhí)行任務(wù)的列表樟凄; -
shutdown
只是將線程池的狀態(tài)設(shè)置為SHUTDOWN狀態(tài),然后中斷所有沒(méi)有正在執(zhí)行任務(wù)的線程
可以看出shutdown方法會(huì)將正在執(zhí)行的任務(wù)繼續(xù)執(zhí)行完兄渺,而shutdownNow會(huì)直接中斷正在執(zhí)行的任務(wù)缝龄。調(diào)用了這兩個(gè)方法的任意一個(gè),isShutdown
方法都會(huì)返回true挂谍,當(dāng)所有的線程都關(guān)閉成功叔壤,才表示線程池成功關(guān)閉,這時(shí)調(diào)用isTerminated
方法才會(huì)返回true口叙。
5. 如何合理配置線程池參數(shù)炼绘?
要想合理的配置線程池,就必須首先分析任務(wù)特性妄田,可以從以下幾個(gè)角度來(lái)進(jìn)行分析:
- 任務(wù)的性質(zhì):CPU密集型任務(wù)俺亮,IO密集型任務(wù)和混合型任務(wù)。
- 任務(wù)的優(yōu)先級(jí):高疟呐,中和低脚曾。
- 任務(wù)的執(zhí)行時(shí)間:長(zhǎng),中和短启具。
- 任務(wù)的依賴性:是否依賴其他系統(tǒng)資源本讥,如數(shù)據(jù)庫(kù)連接。
任務(wù)性質(zhì)不同的任務(wù)可以用不同規(guī)模的線程池分開(kāi)處理。CPU密集型任務(wù)配置盡可能少的線程數(shù)量拷沸,如配置Ncpu+1個(gè)線程的線程池色查。IO密集型任務(wù)則由于需要等待IO操作,線程并不是一直在執(zhí)行任務(wù)撞芍,則配置盡可能多的線程秧了,如2xNcpu∏诼混合型的任務(wù)示惊,如果可以拆分好港,則將其拆分成一個(gè)CPU密集型任務(wù)和一個(gè)IO密集型任務(wù)愉镰,只要這兩個(gè)任務(wù)執(zhí)行的時(shí)間相差不是太大,那么分解后執(zhí)行的吞吐率要高于串行執(zhí)行的吞吐率钧汹,如果這兩個(gè)任務(wù)執(zhí)行時(shí)間相差太大丈探,則沒(méi)必要進(jìn)行分解。我們可以通過(guò)Runtime.getRuntime().availableProcessors()
方法獲得當(dāng)前設(shè)備的CPU個(gè)數(shù)拔莱。
優(yōu)先級(jí)不同的任務(wù)可以使用優(yōu)先級(jí)隊(duì)列PriorityBlockingQueue來(lái)處理碗降。它可以讓優(yōu)先級(jí)高的任務(wù)先得到執(zhí)行,需要注意的是如果一直有優(yōu)先級(jí)高的任務(wù)提交到隊(duì)列里塘秦,那么優(yōu)先級(jí)低的任務(wù)可能永遠(yuǎn)不能執(zhí)行讼渊。
執(zhí)行時(shí)間不同的任務(wù)可以交給不同規(guī)模的線程池來(lái)處理,或者也可以使用優(yōu)先級(jí)隊(duì)列尊剔,讓執(zhí)行時(shí)間短的任務(wù)先執(zhí)行爪幻。
依賴數(shù)據(jù)庫(kù)連接池的任務(wù),因?yàn)榫€程提交SQL后需要等待數(shù)據(jù)庫(kù)返回結(jié)果须误,如果等待的時(shí)間越長(zhǎng)CPU空閑時(shí)間就越長(zhǎng)挨稿,那么線程數(shù)應(yīng)該設(shè)置越大,這樣才能更好的利用CPU京痢。
并且奶甘,阻塞隊(duì)列最好是使用有界隊(duì)列,如果采用無(wú)界隊(duì)列的話祭椰,一旦任務(wù)積壓在阻塞隊(duì)列中的話就會(huì)占用過(guò)多的內(nèi)存資源臭家,甚至?xí)沟孟到y(tǒng)崩潰。
參考文獻(xiàn)
《Java并發(fā)編程的藝術(shù)》
ThreadPoolExecutor源碼分析方淤,很詳細(xì)