線程池(ThreadPoolExecutor)使用學習

線程池現在是面試比較基礎的知識點了,很多人可能都會去學一下線程池的幾個參數,但是對真實使用過程中的問題卻思考的不多。這裏我們來仔細分析一下線程池的一些問題。

爲什麼要用線程池

我們當然可以手動創建,像下面這樣:

    public void threadCreateTest() {
        for (int i = 0; i < 1000; i++) {
            final int x = i;
            new Thread(() -> {
                System.out.println("thread-" + x);
            }).start();
        }
    }

也可以用線程池,

    public void threadPoolTest() {
        ThreadPoolExecutor threadPoolExecutor = new
                ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < 1000; i++) {
            final int x = i;
            threadPoolExecutor.submit(() -> System.out.println("thread-" + x));
        }
    }

但是,對於上面兩個功能大致相同的方法,其中一個要創建1000個線程,另外一個只需要創建5個即可。同一個電腦上統計這兩個的執行時間第一個time: 166ms,第二個time: 91ms,可見,並不是說創建的線程越多,執行速度就越快(線程的創建過程也會消耗系統資源)。

怎麼用

創建線程池

可以使用Executors來創建線程池,些方法創建出來的線程池主要有四種:

  • newFixedThreadPool

    固定線程數量的線程池,BlockingQueue使用的是LinkedBlockingQueue(無長度限制)

  • newSingleThreadExecutor

    創建一個只有一個worker線程的線程池,BlockingQueue使用的是LinkedBlockingQueue

  • newCachedThreadPool

    創建一個按需創建新線程的線程池,會複用之前創建的線程池。此線程池的BlockingQueue使用的是非公平的SynchronousQueue

  • newScheduledThreadPool

    使用的BlockingQueueDelayedWorkQueue

  • newWorkStealingPool

    1.8新增加的,不是ThreadPoolExecutor,而是ForkJoinPool,會創建一個含有足夠多線程的線程池,來維持相應的並行級別,它會通過工作竊取的方式,使得多核的 CPU 不會閒置,總會有活着的線程讓 CPU 去運行。

上面是使用JDK提供的線程池工廠來創建線程池,也可以自己使用ThreadPoolExecutor的構造函數去創建線程池。

image-20220308103339053

線程池參數

其實ThreadPoolExecutor裏面的註釋已經講的很清楚了,構造函數一共有7個

  • corePoolSize(核心線程數),在一般情況下,當執行任務的線程數量達到核心線程數之後,即便這裏面的某些線程是空閒的,也仍會存在,除非設置了allowCoreThreadTimeOut參數

  • maximumPoolSize(最大線程數),當前正在執行任務的線程數量等於核心線程數,且隊列已滿的情況下才會繼續創建新的線程來執行任務,直到線程數量等於最大線程數。所以如果使用無界阻塞隊列(LinkedBlockingQueue),則maximumPoolSize不會起作用,而如果最大線程數=核心線程數,這就是一個固定線程數的線程池。

  • keepAliveTime(空閒線程存活時間),當前執行任務的線程數大於核心線程數,且部分線程處於空閒狀態,則會被回收,此參數結合TimeUnit即爲空閒多久之後會被回收

  • unit(TimeUnit 時間單位,會轉換爲納秒),結合keepAliveTime使用,用來明確空閒的時間。

  • workQueue(任務隊列)放置需要執行的任務的隊列,必需是BlockingQueue(阻塞隊列)

  • threadFactory(線程工廠)創建線程的工廠,可以指定線程分組、名稱等,如果使用默認的線程工廠,此線程池裏面的線程都屬於一個group。

  • handler(任務拒絕策略RejectedExecutionHandler),即新submit的任務沒有資源執行,線程數量已經達到最大線程數,且任務隊列已滿,提供了四種拒絕策略,默認使用的是AbortPolicy

    • AbortPolicy

      拒絕提交的任務,拋出RejectedExecutionException異常。

    • CallerRunsPolicy

      當前提交任務的線程執行任務。

    • DiscardOldestPolicy

      取消當前未執行且等待(排隊)時間最久的任務(在BlockingQueue中),再交給線程池執行。

    • DiscardPolicy

      只是取消要提交的任務,不拋出異常。

其他方法

多數時候,我們使用線程池只是直接submit,或者有些場景需要等待任務很行完,拿到任務執行的結果,加上CountDownLatch等。

而ThreadPoolExecutor還給我們提供了其他的方法(有很多,get類型的我們略過),只看部分方法。

  • beforeExecute 執行任務前,也是模板方法的一種使用,可以用來重新初始化(re-intialize)ThreadLocals,或者記錄日誌等

  • afterExecute 執行任務後

  • finalize 執行shutdown

  • isShutdown 判斷是不是shutdown isTerminated 所有任務在shutdown後執行完成,返回true,如果沒有先執行過shutdown或者shutdownNow,則肯定不爲true isTerminating 即正在終止中,當調用了shutdown或者shutdownNow後,並沒有完成terminated,返回true。

  • prestartAllCoreThreads 啓動所有核心線程,使它們空閒等待工作,會覆蓋僅在執行新任務時啓動核心線程的默認策略,返回啓動的線程數

  • prestartCoreThread 啓動一個核心線程,使其空閒等待工作,會覆蓋執行新任務時啓動核心線程的默認策略,如果所有核心線程都已啓動,此方法將返回 false

  • purge 嘗試從工作隊列中刪除所有已取消的 Future 任務

  • remove 從任務隊列中移除任務

  • shutdown 有序關閉,執行先前提交的任務,但不會接受新任務

  • **shutdownNow ** 嘗試停止所有正在執行的任務,從queue中把正在等待執行的任務移除

  • terminated 一個用來擴展的方法,停止線程池,默認空實現

線程池參數設置

創建線程池時,我們需要思考如何設置核心線程數與最大線程數

一般來說,需要考慮我們運行的任務是CPU密集型和IO密集型

IO密集型可以多設置一些,如2N(N爲CPU核數)或者更多,比如Tomcat設置的默認線程數是100

CPU密集型設置的是N+1(N爲CPU核數)。

但是如果任務即有IO又有CPU的情況該如何設置,其實我們需要考慮的是讓程序能在有IO的情況下,還能充分利用CPU,如果IO比較多(即多數時候線程等待時間長),那麼就需要更多的線程,反之,則需要較少的線程。

通過查閱資料,發現多數情況下可以參考利特爾法則

最佳線程數目 = ((線程等待時間+線程CPU時間)/線程CPU時間 )* CPU數目

且需要根據實際的測試情況調用。

參考資料:

如何使用利特爾法則調整線程池大小

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章