JUC多线程:线程池的创建及工作原理 和 Executor 框架
一、什么是線程池:
線程池主要是為了解決 新任務(wù)執(zhí)行時,應(yīng)用程序為任務(wù)創(chuàng)建一個新線程 以及 任務(wù)執(zhí)行完畢時,銷毀線程所帶來的開銷。通過線程池,可以在項目初始化時就創(chuàng)建一個線程集合,然后在需要執(zhí)行新任務(wù)時重用這些線程而不是每次都新建一個線程,一旦任務(wù)已經(jīng)完成了,線程回到線程池中并等待下一次分配任務(wù),達到資源復(fù)用的效果。
1、線程池的主要優(yōu)勢有:
(1)降低資源消耗:通過池化技術(shù)重復(fù)利用已創(chuàng)建的線程,降低線程創(chuàng)建和銷毀造成的損耗。
(2)提高響應(yīng)速度:任務(wù)到達時,無需等待線程創(chuàng)建即可立即執(zhí)行。
(3)提高線程的可管理性:線程是稀缺資源,如果無限制創(chuàng)建,不僅會消耗系統(tǒng)資源,還會因為線程的不合理分布導(dǎo)致資源調(diào)度失衡,降低系統(tǒng)的穩(wěn)定性。使用線程池可以進行統(tǒng)一的分配、調(diào)優(yōu)和監(jiān)控。
(4)提供更多更強大的功能:線程池具備可拓展性,允許開發(fā)人員向其中增加更多的功能。比如延時定時線程池ScheduledThreadPoolExecutor,就允許任務(wù)延期執(zhí)行或定期執(zhí)行。
?
二、創(chuàng)建線程池:
1、通過Executors創(chuàng)建線程池:
在JUC包中的Executors中,提供了一些靜態(tài)方法,用于快速創(chuàng)建線程池,常見的線程池有:
(1)newSingleThreadExecutor:創(chuàng)建一個只有一個線程的線程池,串行執(zhí)行所有任務(wù),即使空閑時也不會被關(guān)閉。可以保證所有任務(wù)的執(zhí)行順序按照任務(wù)的提交順序執(zhí)行。如果這個唯一的線程因為異常結(jié)束,那么會有一個新的線程來替代它。
適用場景:需要保證順序地執(zhí)行各個任務(wù);并且在任意時間點,不會有多個線程活動的應(yīng)用場景。
(2)newFixedThreadPool:創(chuàng)建一個固定線程數(shù)量的線程池(corePoolSize == maximumPoolSize,使用LinkedBlockingQuene作為阻塞隊列)。初始化時線程數(shù)量為零,之后每次提交一個任務(wù)就創(chuàng)建一個線程,直到線程達到線程池的最大容量。線程池的大小一旦達到最大值就會保持不變,如果某個線程因為執(zhí)行異常而結(jié)束,那么線程池會補充一個新線程。
適用場景:為了滿足資源管理的需求,而需要限制當(dāng)前線程數(shù)量的應(yīng)用場景,它適用于負載比較重的服務(wù)器。
(3)newCachedThreadPool:創(chuàng)建一個可緩存的線程池,線程的最大數(shù)量為Integer.MAX_VALUE。空閑線程會臨時緩存下來,線程會等待60s還是沒有任務(wù)加入的話就會被關(guān)閉。
適用場景:適用于執(zhí)行很多的短時間異步任務(wù)的小程序,或者是負載較輕的服務(wù)器。
(4)newScheduledThreadPool:創(chuàng)建一個支持執(zhí)行延遲任務(wù)或者周期性執(zhí)行任務(wù)的線程池。
2、ThreadPoolExecutor構(gòu)造函數(shù)參數(shù)的說明:
使用Executors創(chuàng)建的線程池,其本質(zhì)都是通過不同的參數(shù)構(gòu)造一個ThreadPoolExecutor對象,主要包含以下7個參數(shù):
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) {// 省略...}(1)corePoolSize:線程池中的核心線程數(shù),當(dāng)提交一個任務(wù)時,線程池創(chuàng)建一個新線程執(zhí)行任務(wù),直到當(dāng)前線程數(shù)等于corePoolSize;如果當(dāng)前線程數(shù)為corePoolSize,繼續(xù)提交的任務(wù)被保存到阻塞隊列workQueue中,等待被執(zhí)行;如果執(zhí)行了線程池的prestartAllCoreThreads()方法,線程池會提前創(chuàng)建并啟動所有核心線程。
(2)maximumPoolSize:線程池中允許的最大線程數(shù)。如果當(dāng)前workQueue滿了之后可以創(chuàng)建的最大線程數(shù)。
(3)keepAliveTime:空閑線程的存活時間。
(4)unit:keepAliveTime空閑線程存活時間的單位;
(5)workQueue:阻塞隊列,用來存放等待被執(zhí)行的任務(wù),且任務(wù)必須實現(xiàn)Runnable接口,在JDK中提供了如下阻塞隊列:
- ArrayBlockingQueue:基于數(shù)組結(jié)構(gòu)的有界阻塞隊列,按FIFO排序任務(wù);
- LinkedBlockingQuene:基于鏈表結(jié)構(gòu)的阻塞隊列,按FIFO排序任務(wù),吞吐量通常要高于ArrayBlockingQuene;
- SynchronousQuene:一個不存儲元素的阻塞隊列,每個插入操作必須等到另一個線程調(diào)用移除操作,否則插入操作一直處于阻塞狀態(tài),吞吐量通常要高于LinkedBlockingQuene
- PriorityBlockingQuene:具有優(yōu)先級的無界阻塞隊列;
- DelayQueue:一個使用優(yōu)先級隊列實現(xiàn)的無界阻塞隊列,只有在延遲期滿時才能從中提取元素。
- LinkedTransferQueue:一個由鏈表結(jié)構(gòu)組成的無界阻塞隊列。與SynchronousQueue類似,還含有非阻塞方法。
- LinkedBlockingDeque:一個由鏈表結(jié)構(gòu)組成的雙向阻塞隊列。
(6)threadFactory:線程工廠,主要用來創(chuàng)建線程,默認(rèn)為正常優(yōu)先級、非守護線程。
(7)handler:線程池拒絕任務(wù)時的處理策略。
3、不要使用Executors創(chuàng)建線程池:
阿里巴巴開發(fā)手冊并發(fā)編程有一條規(guī)定:線程池不允許使用Executors去創(chuàng)建,而是通過ThreadPoolExecutor的方式,這是為什么呢?主要是因為這樣的可以避免資源耗盡的風(fēng)險,因為使用Executors返回線程池對象的弊端有:
(1)FixedThreadPool 和 SingleThreadPool 允許的阻塞隊列長度為 Integer.MAX_VALUE,這樣會導(dǎo)致堆積大量的請求,從而導(dǎo)致OOM;
(2)CachedThreadPool 允許創(chuàng)建的線程數(shù)量為 Integer.MAX_VALUE,可能會創(chuàng)建大量的線程,從而導(dǎo)致 OOM。
所以創(chuàng)建線程池,最好是根據(jù)線程池的用途,然后自己創(chuàng)建線程池。
?
三、線程池執(zhí)行策略:
執(zhí)行邏輯說明:
(1)當(dāng)客戶端提交任務(wù)時,線程池先判斷核心線程數(shù)是否小于corePoolSize,如果是,則創(chuàng)建新的核心線程數(shù)運行這個任務(wù);
(2)如果正在運行的線程數(shù)大于或等于corePoolSize,則判斷workQueue隊列是否已滿,如果未滿,則將任務(wù)放入workQueue中;
(3)如果workQueue隊列已經(jīng)滿了,則判斷當(dāng)前線程池中的線程數(shù)量是否大于maximumPoolSize,如果小于maximumPoolSize,則啟動一個非核心線程來執(zhí)行任務(wù);
(4)如果線程池中線程數(shù)量大于或等于maximumPoolSize,那么線程池會根據(jù)設(shè)定的拒絕策略,做出相應(yīng)的措施。
- ThreadPoolExecutor.AbortPolic(默認(rèn)):拋出RejectedExecutionException異常;
- ThereadPoolExecutor.CallerRunsPolicy:在當(dāng)前正在執(zhí)行的線程的execute方法中運行被拒絕的任務(wù)。
- ThreadPoolExecutor.DiscardOldestPoliy:丟棄workQueue中等待最長時間的任務(wù),并將被拒絕的任務(wù)添加到隊列之中。
- ThreadPoolExecutor.DiscardPolicy:將直接丟棄此線程。
(5)當(dāng)一個線程完成任務(wù)時,它會從workQueue中獲取下一個任務(wù)來執(zhí)行。
(6)當(dāng)一個線程空閑超過keepAliveTime設(shè)定的時間時,線程池會判斷,如果當(dāng)前線程數(shù)大于corePoolSize,那么這個線程就被停掉。所以線程池的所有任務(wù)完成后,它最終會收縮到corePoolSize的大小。
?
四、如何合理的配置線程池的大小?
1、一般都是根據(jù)任務(wù)類型來配置線程池大小:
- 如果是 CPU 密集型:那么就意味著 CPU 是稀缺資源,這個時候通常不能通過增加線程數(shù)來提高計算能力,因為線程數(shù)量太多,會導(dǎo)致頻繁的上下文切換,一般這種情況下,建議合理的線程數(shù)值 = CPU數(shù) + 1,減少線程上下文的切換;
- 如果是 IO 密集型:說明需要較多的等待,因為 IO 操作并不占用CPU,大部分線程都阻塞,所以可以多配置線程數(shù),讓CPU處理更多的業(yè)務(wù),這個時候可以參考 Brain Goetz 的推薦方法,線程數(shù) = CPU核數(shù) × (1 + 平均等待時間/平均工作時間)。參考值可以是 N(CPU) 核數(shù) * 2。
當(dāng)然,這只是一個參考值,具體的設(shè)置還需要根據(jù)實際情況進行調(diào)整,比如可以先將線程池大小設(shè)置為參考值,再觀察任務(wù)運行情況和系統(tǒng)負載、資源利用率來進行適當(dāng)調(diào)整。
2、有界隊列和無界隊列的配置:
一般情況下配置有界隊列,在一些可能會有爆發(fā)性增長的情況下使用無界隊列。任務(wù)非常多時,使用非阻塞隊列并使用CAS操作替代鎖可以獲得好的吞吐量。
?
五、Executor 框架:
1、什么是 Executor 框架:
Executor 是一個用于任務(wù)執(zhí)行和調(diào)度的框架,目的是將任務(wù)的提交過程與執(zhí)行過程解耦,使得用戶只需關(guān)注任務(wù)的定義和提交,而不需要關(guān)注具體如何執(zhí)行以及何時執(zhí)行;其中,最頂層是 Executor 接口,它只有一個用于執(zhí)行任務(wù)的 execute() 方法。Executor框架主要由3大部分組成:
- (1)任務(wù):實現(xiàn) Callable 接口或 Runnable 接口的類,其實例就可以成為一個任務(wù)提交給 ExecutorService 去執(zhí)行:其中 Callable 任務(wù)可以返回執(zhí)行結(jié)果,Runnable 任務(wù)無返回結(jié)果。
- (2)任務(wù)的執(zhí)行:包括任務(wù)執(zhí)行機制的核心接口 Executor,以及繼承自 Executor 的 ExecutorService 接口。Executor框架的關(guān)鍵類ThreadPoolExecutor 也實現(xiàn)了 ExecutorService 接口;
- (3)任務(wù)的異步計算結(jié)果:包括 Future 接口和實現(xiàn) Future 接口的 FutureTask 類、ForkJoinTask 類。
2、使用步驟:
把任務(wù),如 Runnable 接口或 Callable 接口的實現(xiàn)類提交(submit、execute)給線程池執(zhí)行,如 ExecutorService、ThreadPoolExecutor 等。線程執(zhí)行完畢之后,會返回一個異步計算結(jié)果 Future,然后調(diào)用 Future 的 get()方法等待執(zhí)行結(jié)果即可,Future 的 get() 方法會導(dǎo)致主線程阻塞,直到任務(wù)執(zhí)行完成。
其中 Runnable 任務(wù)無返回結(jié)果,Callable 任務(wù)可以返回執(zhí)行結(jié)果,Callable 任務(wù)除了返回正常結(jié)果之外,如果發(fā)生異常,該異常也會被返回,即 Future 可以拿到異步執(zhí)行任務(wù)各種結(jié)果;在實際業(yè)務(wù)場景中,Future 和 Callable 基本是成對出現(xiàn)的,Callable 負責(zé)產(chǎn)生結(jié)果,Future 負責(zé)獲取結(jié)果。
另外,還有一個 Executors 類,它是一個工具類,提供了創(chuàng)建 ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable 對象的靜態(tài)方法。
3、線程池中 submit() 和 execute() 方法有什么區(qū)別?
兩個方法都可以向線程池提交任務(wù),execute() 方法的返回類型是 void,它定義在 Executor 接口中, 而 submit() 方法可以返回持有計算結(jié)果的 Future 對象,它定義在 ExecutorService 接口中,它擴展自 Executor 接口,其它線程池類像 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有這些方法。
?
六、Java線程模型:Java線程與操作系統(tǒng)線程的關(guān)系:
現(xiàn)在 Java 線程的本質(zhì),其實就是操作系統(tǒng)中的線程,Java 線程的實現(xiàn)是基于一對一的線程模型,通過語言級別層面程序去間接調(diào)用系統(tǒng)內(nèi)核的線程模型,即在使用 Java 線程時,JVM 是轉(zhuǎn)而調(diào)用當(dāng)前操作系統(tǒng)的內(nèi)核線程來完成當(dāng)前任務(wù)。內(nèi)核線程就是由操作系統(tǒng)內(nèi)核支持的線程,這種線程是由操作系統(tǒng)內(nèi)核來完成線程切換,內(nèi)核通過操作調(diào)度器進而對線程執(zhí)行調(diào)度,并將線程的任務(wù)映射到各個處理器上。
由于我們編寫的多線程程序?qū)儆谡Z言層面的,程序不會直接去調(diào)用內(nèi)核線程,取而代之的是一種輕量級的進程(Light Weight Process),也是通常意義上的線程,由于每個輕量級進程都會映射到一個內(nèi)核線程,因此我們可以通過輕量級進程調(diào)用內(nèi)核線程,進而由操作系統(tǒng)內(nèi)核將任務(wù)映射到各個處理器,這種輕量級進程與內(nèi)核線程間1對1的關(guān)系就稱為一對一的線程模型。
實際上從 Linux 內(nèi)核2.6開始,就把 LinuxThread 從 LWP 換成了新的線程實現(xiàn)方式NPTL,NPTL 解決了 LinuxThread 中絕大多數(shù)跟POSIX 標(biāo)準(zhǔn)不兼容的特性,并提供了更好的性能,可擴展性及可維護性等等。
Java 線程模型如下圖所示,每個線程最終都會映射到CPU中進行處理,如果CPU存在多核,那么一個CPU將可以并行執(zhí)行多個線程任務(wù):
參考文章:
線程池源碼解析:https://juejin.cn/post/6927456645169545230
Java線程和操作系統(tǒng)線程的關(guān)系:https://juejin.cn/post/6918559409496915982
全面理解Java內(nèi)存模型及volatile關(guān)鍵字:https://blog.csdn.net/javazejian/article/details/72772461
總結(jié)
以上是生活随笔為你收集整理的JUC多线程:线程池的创建及工作原理 和 Executor 框架的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ubuntu系统使用Anaconda安装
- 下一篇: 使用LinkedHashMap实现LRU