Java線程池的概述和作用


線程池的作用

我們在用一個東西的時候,首先得搞明白一個問題。這玩意是干嘛的,為啥要用這個,用別的不行嗎。那么一個一個解決這些問題

我們之前都用過數據庫連接池,線程池的作用和連接池有點類似,頻繁的創建,銷毀線程會造成大量的不必要的性能開銷,所以這個時候就出現了一個東西統一的管理線程,去負責線程啥時候銷毀,啥時候創建,以及維持線程的狀態,當程序需要使用線程的時候,直接從線程池拿,當程序用完了之后,直接把線程放回線程池,不需要去管線程的生命周期,專心的執行業務代碼就行。

當然,如果非要是自己想手動new一個線程來執行,也不是不可以,只是像上面說的那樣,第一麻煩,第二開銷大,第三不好控制。

控制線程的方法

在說到線程池之前,首先要提到一個創建線程池的工具類,又或者說是工廠類 Executors 通過這個線程可以統一的創建線程,返回的是一個ExecutorService 類這個類中包含了一些對線程執行過程進行管理控制的方法;

  • void execute(Runnable command); 這個方法是將任務提交到線程池進行執行。這個方法沒有返回值。

  • <T> Future<T> submit(Callable<T> task); 這個方法最特別的地方是線程執行完畢之后是有返回值的,另外方法的參數可以用Callable也可以為Runnable。可以適用於一些后續的代碼,需要線程執行結果的程序。

    下面的示例中,我們創建了一個 ExecutorService 的實例,提交了一個任務,然后使用返回的 Future 的 get() 方法等待提交的任務完成並返回值。

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Future<String> future = executorService.submit(() -> "Hello World");
        // 一些其它操作
        String result = future.get();
    

    在實際使用時,我們並不會立即調用 future.get() 方法,可能會等待一些時間,推遲調用它直到我們需要它的值用於計算等目的。

    ExecutorService 中的 submit() 方法被重載為支持 RunnableCallable ,它們都是功能接口,可以接收一個 lambdas 作為參數( 從 Java 8 開始 ):

    • 使用 Runnable 作為參數的方法不會拋出異常也不會返回任何值 ( 返回 void )
    • 使用 Callable 作為參數的方法則可以拋出異常也可以返回值。

    如果想讓編譯器將參數推斷為 Callable 類型,只需要 lambda 返回一個值即可。

  • void shutdown(); 在調用了shutdown方法之后,線程池就不會再接收新的任務,此時線程池還沒有停止,仍然會把線程池中國正在執行但是還沒有執行完的任務繼續執行完畢,那些沒有開始執行的任務則被中斷

  • List<Runnable> shutdownNow(); 在調用了shutdownNow方法之后,會將線程池的狀態設置為stop,正在執行的任務則被停止,沒被執行任務的則返回。

    這兩種方法的使用場景:如果線程中的任務相互之間沒有什么關聯某個線程的異常對結果影響不大。那么所有線程都能在執行任務結束之后可以正常結束,程序能在所有task都做完之后正常退出,適合用ShutDown。但是,如果一個線程在做某個任務的時候失敗,則整個結果就是失敗的,其他worker再繼續做剩下的任務也是徒勞,這就需要讓他們全部停止當前的工作。這里使用ShutDownNow就可以讓該pool中的所有線程都停止當前的工作,從而迫使所有線程執行退出。從而讓主程序正常退出。

線程池的分類

通過工廠類 Executors 通過這個線程可以根據自己的需要統一的創建各種類型的線程,線程的分類大致分為以下四種:

  1. newSingleThreadExecutor
  2. CachedThreadPool
  3. newFixedThreadPool
  4. newScheduledThreadPool
  • newSingleThreadExecutor 創建一個單線程的線程池,核心線程和最大線程都為1,因此只會有一個工作線程,會按照指定順序去執行,而且空閑時間為0,說明一旦沒有任務了,線程就會被銷毀

    public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>()));
        }
    
    
    public class SinglePoolDemo {
        public static void main(String[] args) {
            ExecutorService pool = Executors.newSingleThreadExecutor();
    //        ExecutorService pool = Executors.newFixedThreadPool(2);
            for (int i = 0; i < 10; i++) {
                int finalI = i;
                pool.execute(() -> {
                    System.out.println(Thread.currentThread().getName()+"----"+ finalI);
                });
            }
        }
    }
    

    輸出結果:

    pool-1-thread-1----0
    pool-1-thread-1----1
    pool-1-thread-1----2
    pool-1-thread-1----3
    pool-1-thread-1----4
    pool-1-thread-1----5
    pool-1-thread-1----6
    pool-1-thread-1----7
    pool-1-thread-1----8
    pool-1-thread-1----9
    

    觀察線程編號,可以發現,自始自終都只有一個線程在執行,並且也是按照順序來執行的,。

  • CachedThreadPool 創建一個按需創建的線程,核心線程數為0,有一個最大線程數量,意味着可以根據實際任務數的需要,靈活的創建和管理線程,keepAlive時間為60s,代表當某線程超過60s空閑的時候,才會被銷毀,這個線程池最特殊的地方在於,同步隊列最多只能有一個元素,加入隊列的線程會被馬上執行。

      public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>());
        }
    
    public class CachePoolDemo {
        public static void main(String[] args) {
    
            ExecutorService pool = Executors.newCachedThreadPool();
            for (int i = 0; i < 20000; i++) {
                int finalI = i;
                pool.submit(() -> {
                    System.out.println(Thread.currentThread().getName()+"-------------"+finalI);
                });
            }
        }
    }
    

    運行結果部分:

    ......
    pool-1-thread-1805-------------19760
    pool-1-thread-1806-------------19783
    pool-1-thread-1809-------------19875
    pool-1-thread-1810-------------19951
    pool-1-thread-1811-------------19980
    

    以上的代碼我們運行了2w次線程任務,如果是按照我們之前的做法的話,我們要new 2w的線程去執行。通過這個不定長的線程池,他可以根據任務數來靈活的分配所創建的線程,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程,所以這里只創建了大概1800多個線程就完成了我們原本需要new 2w個線程才能完成的任務,之所以說他是靈活分配的是因為,可以這樣驗證看看,把i的值改為20的話,所創建的線程數量大概是10以內,因此是根據任務數量來自行創建線程數的,可以保證效率和性能的最大化。

    但是經過實測,這個靈活性雖然最高,但是性能貌似是相對比較差的,在兩萬任務數的條件下,所以他的缺點就是,可能會創建大量的線程。當然線程池這東西是需要根據自身情況來選擇的。如果主線程提交任務的速度遠遠大於CachedThreadPool的處理速度,則CachedThreadPool會不斷地創建新線程來執行任務,這樣有可能會導致系統耗盡CPU和內存資源,所以在使用該線程池是,一定要注意控制並發的任務數,否則創建大量的線程可能導致嚴重的性能問題

  • newFixedThreadPool 可以通過傳入一個int參數來指定創建一個定長的線程池,該線程池的核心線程數和最大線程數都是你傳進去的參數的值,存活時間都為0說明只要任務空閑下來了,就會被銷毀,阻塞隊列的最大值為MAX_VALUE。所以他的缺點是,可能會將大量的時間花在處理堆積的請求阻塞隊列中的線程。

        public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
        }
    
    public class FixedPoolDemo {
        public static void main(String[] args) {
            ExecutorService pool = Executors.newFixedThreadPool(10);
    //        ExecutorService pool = Executors.newFixedThreadPool(2);
            for (int i = 0; i < 1000; i++) {
                int finalI = i;
                pool.execute(() -> {
                    System.out.println(Thread.currentThread().getName()+"----"+ finalI);
                });
            }
        }
    }
    

    從運行結果可以看出,線程池一直都是維持着十個線程

    .....
    pool-1-thread-5----882
    pool-1-thread-1----881
    pool-1-thread-4----865
    pool-1-thread-10----989
    pool-1-thread-3----931
    pool-1-thread-2----934
    pool-1-thread-9----910
    pool-1-thread-6----896
    

  • newScheduledThreadPool 創建一個定長線程池,支持定時及周期性任務執行。

       public ScheduledThreadPoolExecutor(int corePoolSize) {
            super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
                  new DelayedWorkQueue());
        }
    

    以上四種線程池,各有優劣點

    newFixedThreadPool、newSingleThreadExecutor:

    主要問題是堆積的請求處理隊列可能會耗費非常大的內存,甚至OOM。

    newCachedThreadPool、newScheduledThreadPool:

    主要問題是線程數最大數是Integer.MAX_VALUE,可能會創建數量非常多的線程,甚至OOM。

阿里線程池規范

  1. 線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。

  2. FixedThreadPool 和 SingleThreadPool: 允許的請求隊列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。

  3. CachedThreadPool 和 ScheduledThreadPool: 允許的創建線程數量為 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM。

總結

本篇文章首先我們知道了線程池有什么好處,然后了解一些線程的執行方法,submit,execute,shutdown以及他們的區別,用法等等,然后對幾種線程池做了一個大概的介紹,以及他們的作用,好處和弊端。如果看的細心的同學可以看代碼發現,這些線程池其實本質上都是通過創建一個 ThreadPoolExecutor ,包括阿里的線程池規范也是建議用ThreadPoolExecutor ,但是本篇文章只是對線程池的作用以及分類做一個概述,在下篇文章中,將會詳細的講一下ThreadPoolExecutor


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM