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