深度源碼解析Java 線程池的實(shí)現(xiàn)原理
java 系統(tǒng)的運(yùn)行歸根到底是程序的運(yùn)行,程序的運(yùn)行歸根到底是代碼的執(zhí)行,代碼的執(zhí)行歸根到底是虛擬機(jī)的執(zhí)行,虛擬機(jī)的執(zhí)行其實(shí)就是操作系統(tǒng)的線程在執(zhí)行,并且會(huì)占用一定的系統(tǒng)資源,如CPU、內(nèi)存、磁盤、網(wǎng)絡(luò)等等。所以,如何高效的使用這些資源就是程序員在平時(shí)寫代碼時(shí)候的一個(gè)努力的方向。本文要說的線程池就是一種對(duì) CPU 利用的優(yōu)化手段。
線程池,百度百科是這么解釋的:
線程池是一種多線程處理形式,處理過程中將任務(wù)添加到隊(duì)列,然后在創(chuàng)建線程后自動(dòng)啟動(dòng)這些任務(wù)。線程池線程都是后臺(tái)線程。每個(gè)線程都使用默認(rèn)的堆棧大小,以默認(rèn)的優(yōu)先級(jí)運(yùn)行,并處于多線程單元中。如果某個(gè)線程在托管代碼中空閑(如正在等待某個(gè)事件),則線程池將插入另一個(gè)輔助線程來使所有處理器保持繁忙。如果所有線程池線程都始終保持繁忙,但隊(duì)列中包含掛起的工作,則線程池將在一段時(shí)間后創(chuàng)建另一個(gè)輔助線程但線程的數(shù)目永遠(yuǎn)不會(huì)超過最大值。超過最大值的線程可以排隊(duì),但他們要等到其他線程完成后才啟動(dòng)。
線程池,其實(shí)就是維護(hù)了很多線程的池子,類似這樣的技術(shù)還有很多的,例如:HttpClient 連接池、數(shù)據(jù)庫連接池、內(nèi)存池等等。
線程池的優(yōu)點(diǎn)
在 Java 并發(fā)編程框架中的線程池是運(yùn)用場景最多的技術(shù),幾乎所有需要異步或并發(fā)執(zhí)行任務(wù)的程序都可以使用線程池。在開發(fā)過程中,合理地使用線程池能夠帶來至少以下4個(gè)好處。
第一:降低資源消耗。通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗;
第二:提高響應(yīng)速度。當(dāng)任務(wù)到達(dá)時(shí),任務(wù)可以不需要等到線程創(chuàng)建就能立即執(zhí)行;
第三:提高線程的可管理性。線程是稀缺資源,如果無限制地創(chuàng)建,不僅會(huì)消耗系統(tǒng)資源,還會(huì)降低系統(tǒng)的穩(wěn)定性,使用線程池可以進(jìn)行統(tǒng)一分配、調(diào)優(yōu)和監(jiān)控。
第四:提供更強(qiáng)大的功能,比如延時(shí)定時(shí)線程池;
線程池的實(shí)現(xiàn)原理
當(dāng)向線程池提交一個(gè)任務(wù)之后,線程池是如何處理這個(gè)任務(wù)的呢?下面就先來看一下它的主要處理流程。先來看下面的這張圖,然后我們一步一步的來解釋。
當(dāng)使用者將一個(gè)任務(wù)提交到線程池以后,線程池是這么執(zhí)行的:
①首先判斷核心的線程數(shù)是否已滿,如果沒有滿,那么就去創(chuàng)建一個(gè)線程去執(zhí)行該任務(wù);否則請(qǐng)看下一步
②如果線程池的核心線程數(shù)已滿,那么就繼續(xù)判斷任務(wù)隊(duì)列是否已滿,如果沒滿,那么就將任務(wù)放到任務(wù)隊(duì)列中;否則請(qǐng)看下一步
③如果任務(wù)隊(duì)列已滿,那么就判斷線程池是否已滿,如果沒滿,那么就創(chuàng)建線程去執(zhí)行該任務(wù);否則請(qǐng)看下一步;
④如果線程池已滿,那么就根據(jù)拒絕策略來做出相應(yīng)的處理;
上面的四步其實(shí)就已經(jīng)將線程池的執(zhí)行原理描述結(jié)束了。如果不明白沒有關(guān)系,先一步一步往下看,上面涉及到的線程池的專有名詞都會(huì)詳細(xì)的介紹到。
我們?cè)谄綍r(shí)的開發(fā)中,線程池的使用基本都是基于ThreadPoolExexutor
類,他的繼承體系是這樣子的:
那既然說在使用中都是基于ThreadPoolExecutor
的那么我們就重點(diǎn)分析這個(gè)類。
至于他構(gòu)造體系中的其他的類或者是接口中的屬性,這里就不去截圖了,完全沒有必要。小伙伴如果實(shí)在想看就自己去打開代碼看一下就行了。
ThreadPoolExecutor
在《阿里巴巴 java 開發(fā)手冊(cè)》中指出了線程資源必須通過線程池提供,不允許在應(yīng)用中自行顯示的創(chuàng)建線程,這樣一方面是線程的創(chuàng)建更加規(guī)范,可以合理控制開辟線程的數(shù)量;另一方面線程的細(xì)節(jié)管理交給線程池處理,優(yōu)化了資源的開銷。
其原文描述如下:
在ThreadPoolExecutor
類中提供了四個(gè)構(gòu)造方法,但是他的四個(gè)構(gòu)造器中,實(shí)際上最終都會(huì)調(diào)用同一個(gè)構(gòu)造器,只不過是在另外三個(gè)構(gòu)造器中,如果有些參數(shù)不傳ThreadPoolExecutor
會(huì)幫你使用默認(rèn)的參數(shù)。所以,我們直接來看這個(gè)完整參數(shù)的構(gòu)造器,來徹底剖析里面的參數(shù)。
public class ThreadPoolExecutor extends AbstractExecutorService { ...... public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0){ throw new IllegalArgumentException(); } if (workQueue == null || threadFactory == null || handler == null){ throw new NullPointerException(); } this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; } }
主要參數(shù)就是下面這幾個(gè):
- corePoolSize:線程池中的核心線程數(shù),包括空閑線程,也就是核心線程數(shù)的大小;
- maximumPoolSize:線程池中允許的最多的線程數(shù),也就是說線程池中的線程數(shù)是不可能超過該值的;
- keepAliveTime:當(dāng)線程池中的線程數(shù)大于 corePoolSize 的時(shí)候,在超過指定的時(shí)間之后就會(huì)將多出 corePoolSize 的的空閑的線程從線程池中刪除;
- unit:keepAliveTime 參數(shù)的單位(常用的秒為單位);
- workQueue:用于保存任務(wù)的隊(duì)列,此隊(duì)列僅保持由 executor 方法提交的任務(wù) Runnable 任務(wù);
- threadFactory:線程池工廠,他主要是為了給線程起一個(gè)標(biāo)識(shí)。也就是為線程起一個(gè)具有意義的名稱;
- handler:拒絕策略
阻塞隊(duì)列
workQueue 有多種選擇,在 JDK 中一共提供了 7 中阻塞對(duì)列,分別為:
- ArrayBlockingQueue : 一個(gè)由數(shù)組結(jié)構(gòu)組成的有界阻塞隊(duì)列。 此隊(duì)列按照先進(jìn)先出(FIFO)的原則對(duì)元素進(jìn)行排序。默認(rèn)情況下不保證訪問者公平地訪問隊(duì)列 ,所謂公平訪問隊(duì)列是指阻塞的線程,可按照阻塞的先后順序訪問隊(duì)列。非公平性是對(duì)先等待的線程是不公平的,當(dāng)隊(duì)列可用時(shí),阻塞的線程都可以競爭訪問隊(duì)列的資格。
- LinkedBlockingQueue : 一個(gè)由鏈表結(jié)構(gòu)組成的有界阻塞隊(duì)列。 此隊(duì)列的默認(rèn)和最大長度為Integer.MAX_VALUE。 此隊(duì)列按照先進(jìn)先出的原則對(duì)元素進(jìn)行排序。
- PriorityBlockingQueue : 一個(gè)支持優(yōu)先級(jí)排序的無界阻塞隊(duì)列。 (雖然此隊(duì)列邏輯上是無界的,但是資源被耗盡時(shí)試圖執(zhí)行 add 操作也將失敗,導(dǎo)致 OutOfMemoryError)
- DelayQueue: 一個(gè)使用優(yōu)先級(jí)隊(duì)列實(shí)現(xiàn)的無界阻塞隊(duì)列。 元素的一個(gè)無界阻塞隊(duì)列,只有在延遲期滿時(shí)才能從中提取元素
- SynchronousQueue: 一個(gè)不存儲(chǔ)元素的阻塞隊(duì)列。 一種阻塞隊(duì)列,其中每個(gè)插入操作必須等待另一個(gè)線程的對(duì)應(yīng)移除操作 ,反之亦然。(SynchronousQueue 該隊(duì)列不保存元素)
- LinkedTransferQueue: 一個(gè)由鏈表結(jié)構(gòu)組成的無界阻塞隊(duì)列。 相對(duì)于其他阻塞隊(duì)列LinkedTransferQueue多了tryTransfer和transfer方法。
- LinkedBlockingDeque: 一個(gè)由鏈表結(jié)構(gòu)組成的雙向阻塞隊(duì)列。 是一個(gè)由鏈表結(jié)構(gòu)組成的雙向阻塞隊(duì)列
在以上的7個(gè)隊(duì)列中,線程池中常用的是ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue
,
隊(duì)列中的常用的方法如下:
類型 | 方法 | 含義 | 特點(diǎn) |
---|---|---|---|
拋異常 | add | 添加一個(gè)元素 | 如果隊(duì)列滿,拋出異常 IllegalStateException |
拋異常 | remove | 返回并刪除隊(duì)列的頭節(jié)點(diǎn) | 如果隊(duì)列空,拋出異常 NoSuchElementException |
拋異常 | element | 返回隊(duì)列頭節(jié)點(diǎn) | 如果隊(duì)列空,拋出異常 NoSuchElementException |
不拋異常,但是不阻塞 | offer | 添加一個(gè)元素 | 添加成功,返回 true,添加失敗,返回 false |
不拋異常,但是不阻塞 | poll | 返回并刪除隊(duì)列的頭節(jié)點(diǎn) | 如果隊(duì)列空,返回 null |
不拋異常,但是不阻塞 | peek | 返回隊(duì)列頭節(jié)點(diǎn) | 如果隊(duì)列空,返回 null |
阻塞 | put | 添加一個(gè)元素 | 如果隊(duì)列滿,阻塞 |
阻塞 | take | 返回并刪除隊(duì)列的頭節(jié)點(diǎn) | 如果隊(duì)列空,阻塞 |
關(guān)于阻塞隊(duì)列,介紹到這里也就基本差不多了。
線程池工廠
線程池工廠,就像上面已經(jīng)介紹的,目的是為了給線程起一個(gè)有意義的名字。用起來也非常的簡單,只需要實(shí)現(xiàn)ThreadFactory
接口即可
public class CustomThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName("我是你們自己定義的線程名稱"); return thread; } }
具體的使用就不去廢話了。
拒絕策略
線程池有四種默認(rèn)的拒絕策略,分別為:
- AbortPolicy:這是線程池默認(rèn)的拒絕策略,在任務(wù)不能再提交的時(shí)候,拋出異常,及時(shí)反饋程序運(yùn)行狀態(tài)。如果是比較關(guān)鍵的業(yè)務(wù),推薦使用此拒絕策略,這樣子在系統(tǒng)不能承載更大的并發(fā)量的時(shí)候,能夠及時(shí)的通過異常發(fā)現(xiàn);
- DiscardPolicy:丟棄任務(wù),但是不拋出異常。如果線程隊(duì)列已滿,則后續(xù)提交的任務(wù)都會(huì)被丟棄,且是靜默丟棄。這玩意不建議使用;
- DiscardOldestPolicy:丟棄隊(duì)列最前面的任務(wù),然后重新提交被拒絕的任務(wù)。這玩意不建議使用;
- CallerRunsPolicy:如果任務(wù)添加失敗,那么主線程就會(huì)自己調(diào)用執(zhí)行器中的 executor 方法來執(zhí)行該任務(wù)。這玩意不建議使用;
也就是說關(guān)于線程池的拒絕策略,最好使用默認(rèn)的。這樣能夠及時(shí)發(fā)現(xiàn)異常。如果上面的都不能滿足你的需求,你也可以自定義拒絕策略,只需要實(shí)現(xiàn) RejectedExecutionHandler
接口即可
public class CustomRejection implements RejectedExecutionHandler { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { System.out.println("你自己想怎么處理就怎么處理"); } }
看到這里,我們?cè)賮懋嬕粡垐D來總結(jié)和概括下線程池的執(zhí)行示意圖:
詳細(xì)的執(zhí)行過程全部在圖中說明了。
提交任務(wù)到線程池
在 java 中,有兩個(gè)方法可以將任務(wù)提交到線程池,分別是submit
和execute
。
execute 方法
execute()方法用于提交不需要返回值的任務(wù),所以無法判斷任務(wù)是否被線程池執(zhí)行成功。
void execute(Runnable command);
通過以下代碼可知 execute() 方法輸入的任務(wù)是一個(gè)Runnable類的實(shí)例。
executorService.execute(()->{ System.out.println("ThreadPoolDemo.execute"); });
submit 方法
submit()方法用于提交需要返回值的任務(wù)。
Future<?> submit(Runnable task);
線程池會(huì)返回一個(gè)future類型的對(duì)象,通過這個(gè) future 對(duì)象可以判斷任務(wù)是否執(zhí)行成功,并且可以通過future的get()方法來獲取返回值,get() 方法會(huì)阻塞當(dāng)前線程直到任務(wù)完成,而使用get(long timeout,TimeUnit unit)方法則會(huì)阻塞當(dāng)前線程一段時(shí)間后立即返回,這時(shí)候有可能任務(wù)沒有執(zhí)行完。
Future<?> submit = executorService.submit(() -> { System.out.println("ThreadPoolDemo.submit"); });
關(guān)閉線程池
其實(shí),如果優(yōu)雅的關(guān)閉線程池是一個(gè)令人頭疼的問題,線程開啟是簡單的,但是想要停止卻不是那么容易的。通常而言, 大部分程序員都是使用 jdk 提供的兩個(gè)方法來關(guān)閉線程池,他們分別是:shutdown
或 shutdownNow
;
通過調(diào)用線程池的 shutdown
或 shutdownNow
方法來關(guān)閉線程池。它們的原理是遍歷線程池中的工作線程,然后逐個(gè)調(diào)用線程的 interrupt 方法來中斷線程(PS:中斷,僅僅是給線程打上一個(gè)標(biāo)記,并不是代表這個(gè)線程停止了,如果線程不響應(yīng)中斷,那么這個(gè)標(biāo)記將毫無作用),所以無法響應(yīng)中斷的任務(wù)可能永遠(yuǎn)無法終止。
但是它們存在一定的區(qū)別,shutdownNow
首先將線程池的狀態(tài)設(shè)置成 STOP,然后嘗試停止所有的正在執(zhí)行或暫停任務(wù)的線程,并返回等待執(zhí)行任務(wù)的列表,而 shutdown
只是將線程池的狀態(tài)設(shè)置成SHUTDOWN
狀態(tài),然后中斷所有沒有正在執(zhí)行任務(wù)的線程。
只要調(diào)用了這兩個(gè)關(guān)閉方法中的任意一個(gè),isShutdown
方法就會(huì)返回 true。當(dāng)所有的任務(wù)都已關(guān)閉后,才表示線程池關(guān)閉成功,這時(shí)調(diào)用isTerminaed
方法會(huì)返回 true。至于應(yīng)該調(diào)用哪一種方法來關(guān)閉線程池,應(yīng)該由提交到線程池的任務(wù)特性決定,通常調(diào)用 shutdown
方法來關(guān)閉線程池,如果任務(wù)不一定要執(zhí)行完,則可以調(diào)用 shutdownNow
方法。
這里推薦使用穩(wěn)妥的 shutdownNow
來關(guān)閉線程池,至于更優(yōu)雅的方式我會(huì)在以后的并發(fā)編程設(shè)計(jì)模式中的兩階段終止模式中會(huì)再次詳細(xì)介紹。
合理的參數(shù)
為什么叫合理的參數(shù),那不合理的參數(shù)是什么樣子的?在我們創(chuàng)建線程池的時(shí)候,里面的參數(shù)該如何設(shè)置才能稱之為合理呢?其實(shí)這是有一定的依據(jù)的,我們先來看一下以下的創(chuàng)建的方式:
ExecutorService executorService = new ThreadPoolExecutor(5, 5, 5, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), r -> { Thread thread = new Thread(r); thread.setName("線程池原理講解"); return thread; });
你說他合理不合理?我也不知道,因?yàn)槲覀儧]有參考的依據(jù),在實(shí)際的開發(fā)中,我們需要根據(jù)任務(wù)的性質(zhì)(IO是否頻繁?)來決定我們創(chuàng)建的核心的線程數(shù)的大小,實(shí)際上可以從以下的一個(gè)角度來分析:
- 任務(wù)的性質(zhì):CPU密集型任務(wù)、IO密集型任務(wù)和混合型任務(wù);
- 任務(wù)的優(yōu)先級(jí):高、中和低;
- 任務(wù)的執(zhí)行時(shí)間:長、中和短;
- 任務(wù)的依賴性:是否依賴其他系統(tǒng)資源,如數(shù)據(jù)庫連接;
性質(zhì)不同的任務(wù)可以用不同規(guī)模的線程池分開處理。分為CPU密集型和IO密集型。
CPU密集型任務(wù)應(yīng)配置盡可能小的線程,如配置 Ncpu+1
個(gè)線程的線程池。(可以通過Runtime.getRuntime().availableProcessors()
來獲取CPU物理核數(shù))
IO密集型任務(wù)線程并不是一直在執(zhí)行任務(wù),則應(yīng)配置盡可能多的線程,如 2*Ncpu
。
混合型的任務(wù),如果可以拆分,將其拆分成一個(gè)CPU密集型任務(wù)一個(gè)IO密集型任務(wù),只要這兩個(gè)任務(wù)執(zhí)行的時(shí)間相差不是太大,那么分解后執(zhí)行的吞吐量將高于串行執(zhí)行的吞吐量。
如果這兩個(gè)任務(wù)執(zhí)行時(shí)間相差太大,則沒必要進(jìn)行分解??梢酝ㄟ^ Runtime.getRuntime().availableProcessors()
方法獲得當(dāng)前設(shè)備的CPU個(gè)數(shù)。
優(yōu)先級(jí)不同的任務(wù)可以使用優(yōu)先級(jí)隊(duì)列 PriorityBlockingQueue
來處理。它可以讓優(yōu)先級(jí)高的任務(wù)先執(zhí)行(注意:如果一直有優(yōu)先級(jí)高的任務(wù)提交到隊(duì)列里,那么優(yōu)先級(jí)低的任務(wù)可能永遠(yuǎn)不能執(zhí)行)
執(zhí)行時(shí)間不同的任務(wù)可以交給不同規(guī)模的線程池來處理,或者可以使用優(yōu)先級(jí)隊(duì)列,讓執(zhí)行時(shí)間短的任務(wù)先執(zhí)行。依賴數(shù)據(jù)庫連接池的任務(wù),因?yàn)榫€程提交SQL后需要等待數(shù)據(jù)庫返回結(jié)果,等待的時(shí)間越長,則 CPU 空閑時(shí)間就越長,那么線程數(shù)應(yīng)該設(shè)置得越大,這樣才能更好地利用CPU。
建議使用有界隊(duì)列。有界隊(duì)列能增加系統(tǒng)的穩(wěn)定性和預(yù)警能力,可以根據(jù)需要設(shè)大一點(diǎn)。方式因?yàn)樘峤坏娜蝿?wù)過多而導(dǎo)致 OOM;
7、本文小結(jié)
本文主要介紹的是線程池的實(shí)現(xiàn)原理以及一些使用技巧,在實(shí)際開發(fā)中,線程池可以說是稍微高級(jí)一點(diǎn)的程序員的必備技能。所以掌握好線程池這門技術(shù)也是重中之重!
以上就是深度源碼解析Java 線程池的實(shí)現(xiàn)原理的詳細(xì)內(nèi)容,更多關(guān)于Java 線程池的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
mybatis if傳入字符串?dāng)?shù)字踩坑記錄及解決
這篇文章主要介紹了mybatis if傳入字符串?dāng)?shù)字踩坑記錄及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02commons fileupload實(shí)現(xiàn)文件上傳的實(shí)例代碼
這篇文章主要介紹了commons fileupload實(shí)現(xiàn)文件上傳的實(shí)例代碼,包括文件上傳的原理分析等相關(guān)知識(shí)點(diǎn),本文給大家介紹的非常詳細(xì),具有參考借鑒價(jià)值,感興趣的朋友一起看看吧2016-10-10springBoot?@Scheduled實(shí)現(xiàn)多個(gè)任務(wù)同時(shí)開始執(zhí)行
這篇文章主要介紹了springBoot?@Scheduled實(shí)現(xiàn)多個(gè)任務(wù)同時(shí)開始執(zhí)行,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12Java項(xiàng)目如何引入日志生成器及其日志分級(jí)
這篇文章主要介紹了Java項(xiàng)目引入日志生成器及其日志分級(jí),本文結(jié)合示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-12-12springMVC利用FastJson接口返回json數(shù)據(jù)相關(guān)配置詳解
本篇文章主要介紹了springMVC利用FastJson接口返回json數(shù)據(jù)相關(guān)配置詳解,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06Java8新特性stream和parallelStream區(qū)別
這篇文章主要介紹了Java8新特性stream和parallelStream區(qū)別,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11