1. 為什么要使用線程池
在實際使用中古胆,線程是很占用系統(tǒng)資源的甩骏,如果對線程管理不善很容易導致系統(tǒng)問題揖庄。因此,在大多數(shù)并發(fā)框架中都會使用線程池來管理線程溪猿,使用線程池管理線程主要有如下好處:
降低資源消耗钩杰。通過復用已存在的線程和降低線程關閉的次數(shù)來盡可能降低系統(tǒng)性能損耗;
提升系統(tǒng)響應速度诊县。通過復用線程讲弄,省去創(chuàng)建線程的過程,因此整體上提升了系統(tǒng)的響應速度依痊;
提高線程的可管理性避除。線程是稀缺資源怎披,如果無限制的創(chuàng)建,不僅會消耗系統(tǒng)資源瓶摆,還會降低系統(tǒng)的穩(wěn)定性凉逛,因此,需要使用線程池來管理線程群井。
2. 線程池的工作原理
當一個并發(fā)任務提交給線程池状飞,線程池分配線程去執(zhí)行任務的過程如下圖所示:
線程池執(zhí)行流程圖.jpg
從圖可以看出,線程池執(zhí)行所提交的任務過程主要有這樣幾個階段:
先判斷線程池中核心線程池所有的線程是否都在執(zhí)行任務书斜。如果不是诬辈,則新創(chuàng)建一個線程執(zhí)行剛提交的任務,否則荐吉,核心線程池中所有的線程都在執(zhí)行任務焙糟,則進入第 2 步;
判斷當前阻塞隊列是否已滿样屠,如果未滿穿撮,則將提交的任務放置在阻塞隊列中;否則痪欲,則進入第 3 步悦穿;
判斷線程池中所有的線程是否都在執(zhí)行任務,如果沒有业踢,則創(chuàng)建一個新的線程來執(zhí)行任務咧党,否則,則交給飽和策略進行處理
3.線程池的實現(xiàn)
不懂的朋友可以看看這個陨亡,線程池現(xiàn)實的視頻講解,點擊:150行代碼深员,手寫線程池
4. 線程池的創(chuàng)建
創(chuàng)建線程池主要是ThreadPoolExecutor類來完成负蠕,ThreadPoolExecutor 的有許多重載的構(gòu)造方法,通過參數(shù)最多的構(gòu)造方法來理解創(chuàng)建線程池有哪些需要配置的參數(shù)倦畅。ThreadPoolExecutor 的構(gòu)造方法為:
ThreadPoolExecutor(int corePoolSize,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? int maximumPoolSize,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? long keepAliveTime,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? TimeUnit unit,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? BlockingQueue<Runnable> workQueue,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ThreadFactory threadFactory,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? RejectedExecutionHandler handler)
下面對參數(shù)進行說明:
corePoolSize:表示核心線程池的大小遮糖。當提交一個任務時,如果當前核心線程池的線程個數(shù)沒有達到 corePoolSize叠赐,則會創(chuàng)建新的線程來執(zhí)行所提交的任務欲账,即使當前核心線程池有空閑的線程。如果當前核心線程池的線程個數(shù)已經(jīng)達到了 corePoolSize芭概,則不再重新創(chuàng)建線程赛不。如果調(diào)用了prestartCoreThread()或者 prestartAllCoreThreads(),線程池創(chuàng)建的時候所有的核心線程都會被創(chuàng)建并且啟動罢洲。
maximumPoolSize:表示線程池能創(chuàng)建線程的最大個數(shù)踢故。如果當阻塞隊列已滿時,并且當前線程池線程個數(shù)沒有超過 maximumPoolSize 的話,就會創(chuàng)建新的線程來執(zhí)行任務殿较。
keepAliveTime:空閑線程存活時間耸峭。如果當前線程池的線程個數(shù)已經(jīng)超過了 corePoolSize,并且線程空閑時間超過了 keepAliveTime 的話淋纲,就會將這些空閑線程銷毀劳闹,這樣可以盡可能降低系統(tǒng)資源消耗。
unit:時間單位洽瞬。為 keepAliveTime 指定時間單位本涕。
workQueue:阻塞隊列。用于保存任務的阻塞隊列片任,關于阻塞隊列可以看這篇文章偏友。可以使用ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue对供。
threadFactory:創(chuàng)建線程的工程類位他。可以通過指定線程工廠為每個創(chuàng)建出來的線程設置更有意義的名字产场,如果出現(xiàn)并發(fā)問題鹅髓,也方便查找問題原因。
handler:飽和策略京景。當線程池的阻塞隊列已滿和指定的線程都已經(jīng)開啟窿冯,說明當前線程池已經(jīng)處于飽和狀態(tài)了,那么就需要采用一種策略來處理這種情況确徙。采用的策略有這幾種:
AbortPolicy: 直接拒絕所提交的任務醒串,并拋出RejectedExecutionException異常;
CallerRunsPolicy:只用調(diào)用者所在的線程來執(zhí)行任務鄙皇;
DiscardPolicy:不處理直接丟棄掉任務芜赌;
DiscardOldestPolicy:丟棄掉阻塞隊列中存放時間最久的任務,執(zhí)行當前任務
分享更多關于C/C++ Linux后端開發(fā)網(wǎng)絡底層原理知識學習提升? 學習資料伴逸,完善技術棧缠沈,內(nèi)容知識點包括Linux,Nginx错蝴,ZeroMQ洲愤,MySQL,Redis顷锰,線程池柬赐,MongoDB,ZK官紫,流媒體躺率,音視頻玛界,Linux內(nèi)核,CDN悼吱,P2P慎框,epoll,Docker后添,TCP/IP笨枯,協(xié)程,DPDK等等遇西。
線程池執(zhí)行邏輯
通過 ThreadPoolExecutor 創(chuàng)建線程池后馅精,提交任務后執(zhí)行過程是怎樣的,下面來通過源碼來看一看粱檀。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();
//如果線程池的線程個數(shù)少于corePoolSize則創(chuàng)建新線程執(zhí)行當前任務
? ? if (workerCountOf(c) < corePoolSize) {
? ? ? ? if (addWorker(command, true))
? ? ? ? ? ? return;
? ? ? ? c = ctl.get();
? ? }
//如果線程個數(shù)大于corePoolSize或者創(chuàng)建線程失敗洲敢,則將任務存放在阻塞隊列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);
? ? }
//如果當前任務無法放進阻塞隊列中,則創(chuàng)建新的線程來執(zhí)行任務
? ? else if (!addWorker(command, false))
? ? ? ? reject(command);
}
ThreadPoolExecutor 的 execute 方法執(zhí)行邏輯請見注釋茄蚯。下圖為 ThreadPoolExecutor 的 execute 方法的執(zhí)行示意圖:
execute執(zhí)行過程示意圖.jpg
execute 方法執(zhí)行邏輯有這樣幾種情況:
如果當前運行的線程少于 corePoolSize压彭,則會創(chuàng)建新的線程來執(zhí)行新的任務;
如果運行的線程個數(shù)等于或者大于 corePoolSize渗常,則會將提交的任務存放到阻塞隊列 workQueue 中壮不;
如果當前 workQueue 隊列已滿的話,則會創(chuàng)建新的線程來執(zhí)行任務皱碘;
如果線程個數(shù)已經(jīng)超過了 maximumPoolSize询一,則會使用飽和策略 RejectedExecutionHandler 來進行處理。
需要注意的是癌椿,線程池的設計思想就是使用了核心線程池 corePoolSize健蕊,阻塞隊列 workQueue 和線程池 maximumPoolSize,這樣的緩存策略來處理任務踢俄,實際上這樣的設計思想在需要框架中都會使用绊诲。
5. 線程池的關閉
關閉線程池,可以通過shutdown和shutdownNow這兩個方法褪贵。它們的原理都是遍歷線程池中所有的線程,然后依次中斷線程抗俄。shutdown和shutdownNow還是有不一樣的地方:
shutdownNow首先將線程池的狀態(tài)設置為STOP,然后嘗試停止所有的正在執(zhí)行和未執(zhí)行任務的線程脆丁,并返回等待執(zhí)行任務的列表;
shutdown只是將線程池的狀態(tài)設置為SHUTDOWN狀態(tài)动雹,然后中斷所有沒有正在執(zhí)行任務的線程
可以看出 shutdown 方法會將正在執(zhí)行的任務繼續(xù)執(zhí)行完槽卫,而 shutdownNow 會直接中斷正在執(zhí)行的任務。調(diào)用了這兩個方法的任意一個胰蝠,isShutdown方法都會返回 true歼培,當所有的線程都關閉成功震蒋,才表示線程池成功關閉,這時調(diào)用isTerminated方法才會返回 true躲庄。
5. 如何合理配置線程池參數(shù)查剖?
要想合理的配置線程池,就必須首先分析任務特性噪窘,可以從以下幾個角度來進行分析:
任務的性質(zhì):CPU 密集型任務笋庄,IO 密集型任務和混合型任務。
任務的優(yōu)先級:高倔监,中和低直砂。
任務的執(zhí)行時間:長,中和短浩习。
任務的依賴性:是否依賴其他系統(tǒng)資源静暂,如數(shù)據(jù)庫連接。
任務性質(zhì)不同的任務可以用不同規(guī)模的線程池分開處理谱秽。CPU 密集型任務配置盡可能少的線程數(shù)量洽蛀,如配置Ncpu+1個線程的線程池。IO 密集型任務則由于需要等待 IO 操作弯院,線程并不是一直在執(zhí)行任務辱士,則配置盡可能多的線程,如2xNcpu听绳∷痰猓混合型的任務,如果可以拆分椅挣,則將其拆分成一個 CPU 密集型任務和一個 IO 密集型任務头岔,只要這兩個任務執(zhí)行的時間相差不是太大,那么分解后執(zhí)行的吞吐率要高于串行執(zhí)行的吞吐率鼠证,如果這兩個任務執(zhí)行時間相差太大峡竣,則沒必要進行分解。我們可以通過Runtime.getRuntime().availableProcessors()方法獲得當前設備的 CPU 個數(shù)量九。
優(yōu)先級不同的任務可以使用優(yōu)先級隊列 PriorityBlockingQueue 來處理适掰。它可以讓優(yōu)先級高的任務先得到執(zhí)行,需要注意的是如果一直有優(yōu)先級高的任務提交到隊列里荠列,那么優(yōu)先級低的任務可能永遠不能執(zhí)行类浪。
執(zhí)行時間不同的任務可以交給不同規(guī)模的線程池來處理,或者也可以使用優(yōu)先級隊列肌似,讓執(zhí)行時間短的任務先執(zhí)行费就。
依賴數(shù)據(jù)庫連接池的任務,因為線程提交 SQL 后需要等待數(shù)據(jù)庫返回結(jié)果川队,如果等待的時間越長 CPU 空閑時間就越長力细,那么線程數(shù)應該設置越大睬澡,這樣才能更好的利用 CPU。
并且眠蚂,阻塞隊列最好是使用有界隊列煞聪,如果采用無界隊列的話,一旦任務積壓在阻塞隊列中的話就會占用過多的內(nèi)存資源河狐,甚至會使得系統(tǒng)崩潰米绕。