您好,登錄后才能下訂單哦!
這篇文章主要介紹“常見的線程池有哪些”,在日常操作中,相信很多人在常見的線程池有哪些問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”常見的線程池有哪些”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
FixedThreadPool線程池,它的核心線程數和最大線程數是一樣的,所以可以把它看作是固定線程數的線程池。
特點是:線程池中的線程數除了初始階段需要從 0 開始增加外,之后的線程數量就是固定的,就算任務數超過線程數,線程池也不會再創建更多的線程來處理任務,而是會把超出線程處理能力的任務放到任務隊列中進行等待。而且就算任務隊列滿了,到了本該繼續增加線程數的時候,由于它的最大線程數和核心線程數是一樣的,所以也無法再增加新的線程了。
如圖所示,線程池有 t0~t9,10 個線程,它們會不停地執行任務,如果某個線程任務執行完了,就會從任務隊列中獲取新的任務繼續執行,期間線程數量不會增加也不會減少,始終保持在 10 個。
CachedThreadPool,可以稱作可緩存線程池,它的特點在于線程數是幾乎可以無限增加的(實際最大可以達到 Integer.MAX_VALUE,為 2^31-1),而當線程閑置時還可以對線程進行回收。
CachedThreadPool 線程池它也有一個用于存儲提交任務的隊列,但這個隊列是 SynchronousQueue,隊列的容量為0,實際不存儲任何任務,它只負責對任務進行中轉和傳遞,所以效率比較高。
當提交一個任務后,線程池會判斷已創建的線程中是否有空閑線程,如果有空閑線程則將任務直接指派給空閑線程,如果沒有空閑線程,則新建線程去執行任務,這樣就做到了動態地新增線程。如下方代碼所示。
/** * @date 2020/11/15 23:17 * * @discription 緩存線程池 */ public class CachedThreadPool { public static void main(String[] args) { ExecutorService service = Executors.newCachedThreadPool(); for (int i = 0; i < 1000; i++) { service.execute(new ThreadPoolDemo.Task()); } } static class Task implements Runnable{ @Override public void run() { System.out.println("Thread Name: " + Thread.currentThread().getName()); } } }
因為執行任務簡單,所以在 for 循環提交任務結束前,就可能導致先執行的任務被執行完了,線程空閑出來執行后面的任務,從而不會出現線程名稱為pool-1-thread-1000的線程。
假設這些任務處理的時間非常長,因為 for 循環提交任務的操作是非常快的,但執行任務卻比較耗時,就可能導致 1000 個任務都提交完了但第一個任務還沒有被執行完,所以此時 CachedThreadPool 就可以動態的伸縮線程數量,隨著任務的提交,不停地創建 1000 個線程來執行任務,而當任務執行完之后,假設沒有新的任務了,那么大量的閑置線程又會造成內存資源的浪費,這時線程池就會檢測線程在 60 秒內有沒有可執行任務,如果沒有就會被銷毀,最終線程數量會減為 0。
線程池 ScheduledThreadPool,它支持定時或周期性執行任務。
比如每隔 10 秒鐘執行一次任務,而實現這種功能的方法主要有 3 種,如代碼所示:
/** * @date 2020/11/15 23:34 * * @discription 支持定時或周期性執行任務的線程池 */ public class ScheduledThreadPool { public static void main(String[] args) { ScheduledExecutorService service = Executors.newScheduledThreadPool(10); /** * schedule —— 比較簡單表示延遲指定時間后執行一次任務, * 如果代碼中設置參數為 10 秒,也就是 10 秒后執行一次任務后就結束。 */ service.schedule(new ThreadPoolDemo.Task(),10, TimeUnit.SECONDS); /** * scheduleAtFixedRate —— 表示以固定的頻率執行任務, * 它的第二個參數 initialDelay 表示第一次延時時間, * 第三個參數 period 表示周期也就是第一次延時后每次延時多長時間執行一次任務。 */ service.scheduleAtFixedRate(new ThreadPoolDemo.Task(), 10, 10, TimeUnit.SECONDS); /** * scheduleWithFixedDelay —— 與第二種方法類似,也是周期執行任務,區別在于對周期的定義, * 之前的 scheduleAtFixedRate 是以任務開始的時間為時間起點, 開始計時時間到就開始執行第二次任務,而不管任務需要花多久執行; * 而 scheduleWithFixedDelay 方法以任務結束的時間為下一次循環的時間起點開始計時。 */ service.scheduleWithFixedDelay(new ThreadPoolDemo.Task(), 10, 10, TimeUnit.SECONDS); } }
比如:假設某個同學正在熬夜寫代碼,需要喝咖啡來提神,假設每次喝咖啡都需要花10分鐘的時間,如果此時采用第2種方法 scheduleAtFixedRate,時間間隔設置為 1 小時,那么他將會在每個整點喝一杯咖啡,以下是時間表:
00:00: 開始喝咖啡
00:10: 喝完了
01:00: 開始喝咖啡
01:10: 喝完了
02:00: 開始喝咖啡
02:10: 喝完了
采用第3種方法 scheduleWithFixedDelay,時間間隔同樣設置為 1 小時,那么由于每次喝咖啡需要10分鐘,而 scheduleWithFixedDelay 是以任務完成的時間為時間起點開始計時的,所以第2次喝咖啡的時間將會在1:10,而不是1:00整,以下是時間表:
00:00: 開始喝咖啡
00:10: 喝完了
01:10: 開始喝咖啡
01:20: 喝完了
02:20: 開始喝咖啡
02:30: 喝完了
SingleThreadExecutor,它會使用唯一的線程去執行任務,原理和 FixedThreadPool 是一樣的,只不過這里線程只有一個,如果線程在執行任務的過程中發生異常,線程池也會重新創建一個線程來執行后續的任務。
這種線程池由于只有一個線程,所以非常適合用于所有任務都需要按被提交的順序依次執行的場景,而前幾種線程池不一定能夠保障任務的執行順序等于被提交的順序,因為它們是多線程并行執行的。
SingleThreadScheduledExecutor,它實際和第三種 ScheduledThreadPool 線程池非常相似,它只是 ScheduledThreadPool 的一個特例,內部只有一個線程。
注意:它只是將 ScheduledThreadPool 的核心線程數設置為了 1,即new ScheduledThreadPoolExecutor(1),并非最大線程為一的意思。
ForkJoinPool,這個線程池是在 JDK 7 加入的,主要用法和之前的線程池是相同的,也是把任務交給線程池去執行,線程池中也有任務隊列來存放任務。ForkJoinPool 非常適合用于遞歸的場景,例如樹的遍歷、最優路徑搜索等場景。
ForkJoinPool 線程池和之前的線程池有兩點非常大的不同之處。
比如:我們有一個 Task,這個 Task 可以產生三個子任務,三個子任務并行執行完畢后將結果匯總給 Result,比如說主任務需要執行非常繁重的計算任務,我們就可以把計算拆分成三個部分,這三個部分是互不影響相互獨立的,這樣就可以利用 CPU 的多核優勢,并行計算,然后將結果進行匯總。這里面主要涉及兩個步驟,第一步是拆分也就是 Fork,第二步是匯總也就是 Join,這也是ForkJoinPool 線程池名字的由來。
ForkJoinPool 線程池有多種方法可以實現任務的分裂和匯總,其中一種用法計算斐波那契數列如下方代碼所示。
/** * @date 2020/11/16 0:06 * * @discription ForkJoinPool計算斐波那契數列 * 打印出斐波那契數列的第 0 到 9 項的值: */ public class Fibonacci extends RecursiveTask<Integer> { int n; public Fibonacci(int n) { this.n = n; } @Override protected Integer compute() { if(n <= 1){ return n; } Fibonacci f1 = new Fibonacci(n-1); f1.fork(); Fibonacci f2 = new Fibonacci(n-2); f2.fork(); return f1.join() + f2.join(); } public static void main(String[] args) throws ExecutionException, InterruptedException { ForkJoinPool forkJoinPool = new ForkJoinPool(); for (int i = 0; i < 10; i++) { ForkJoinTask task = forkJoinPool.submit(new Fibonacci(i)); System.out.println(task.get()); } } }
它首先繼承了 RecursiveTask,RecursiveTask 類是對ForkJoinTask 的一個簡單的包裝,這時我們重寫 compute() 方法,當 n<=1 時直接返回,當 n>1 就創建遞歸任務,也就是 f1 和 f2,然后我們用 fork() 方法分裂任務并分別執行,最后在 return 的時候,使用 join() 方法把結果匯總,這樣就實現了任務的分裂和匯總。
之前的線程池所有的線程共用一個隊列,但 ForkJoinPool 線程池中每個線程都有自己獨立的任務隊列,如圖所示。
可以看到 ForkJoinPool 線程池和其他線程池很多地方都是一樣的,但重點區別在于除了有一個共用的任務隊列之外,它每個線程都有一個自己的雙端隊列來存儲分裂出來的子任務。
當線程中的任務被 Fork 分裂了,分裂出來的子任務放入線程自己的 deque 里,而不是放入公共的任務隊列中。對于當前線程來說以直接在自己的任務隊列中獲取,而不必去公共隊列中爭搶,也不會發生阻塞(除了 steal 情況外),減少了線程間的競爭和切換,是非常高效的。
若此時線程有多個,而線程 t1 的任務特別繁重,分裂了數十個子任務,但是 t0 此時卻無事可做,它自己的 deque 隊列為空,這時為了提高效率,t0 就會想辦法幫助 t1 執行任務,這就是“work-stealing”的含義。雙端隊列 deque 中,線程 t1 獲取任務的邏輯是后進先出,而線程 t0 在“steal”偷線程 t1 的 deque 中的任務的邏輯是先進先出,使用 “work-stealing” 算法和雙端隊列很好地平衡了各線程的負載。
到此,關于“常見的線程池有哪些”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。