Java Executors四種線程池創建方式對比,解析如何科學的創建線程池

前言

本文主要闡述以下幾個問題:

1、最基礎創建線程的方式

2、Java Executors提供的四種線程池的說明及優缺點對比

3、最科學的線程池創建方式ThreadPoolExecutor

4、如何基於自己的機器合理的設置線程值


1、最基礎創建線程的方式

一般開發中最簡單的創建線程異步操作,new Thread()就行了

 private static void testNewThread() {
        for (int i = 0; i < 10 ; i++) {
            int finalI = i;
            // new Thread() 每次都是新建一個線程去處理,循環10次可能就用了10個線程
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"打印輸出i:"+ finalI);
            }).start();
        }
    }

但是線程無法統一管理,無疑是有很多弊端。

在一個高可用的應用系統中,一般是構建線程池來統一管理維護線程資源


2、Java Executors提供的四種線程池(方法、優缺點對比)

Java通過Executors提供四種線程池,分別爲:

newSingleThreadExecutor 單一線程池,嚴格按順序
newCachedThreadPool 長度無限大,用完的線程可靈活回收,適合耗時短的大量任務。
newFixedThreadPool 可重用固定線程數的線程池,超出的線程會在隊列中等待。
newScheduledThreadPool 支持定時及週期性任務執行。

newSingleThreadExecutor

創建一個單線程化的線程池,它只會用唯一的工作線程來執行異步任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。

   private static void testNewSingleThreadExecutor() {
        // newSingleThreadExecutor 一池只有一個線程,單線程按順序串行執行所有任務
        // 只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行
        // 如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10 ; i++) {
            int finalI = i;
            threadPool.execute(()->{
                System.out.println(Thread.currentThread().getName()+"打印輸出i:"+ finalI);
            });
        }
        threadPool.shutdown();
    }

運行結果如下,可以明顯看到一直是同一線程在執行

pool-1-thread-1打印輸出i:0
pool-1-thread-1打印輸出i:1
pool-1-thread-1打印輸出i:2
pool-1-thread-1打印輸出i:3
pool-1-thread-1打印輸出i:4
pool-1-thread-1打印輸出i:5
pool-1-thread-1打印輸出i:6
pool-1-thread-1打印輸出i:7
pool-1-thread-1打印輸出i:8
pool-1-thread-1打印輸出i:9

newCachedThreadPool

newCachedThreadPool創建一個可緩存的線程池(線程長度爲無限大),調用execute 將重用以前構造的線程(如果線程可用)。如果沒有可用的線程,則創建一個新線程並添加到池中。終止並從緩存中移除那些已有 60 秒鐘未被使用的線程。

    private static void testNewCachedThreadPool() {
        //創建一個可緩存線程池,完成任務後的空閒線程可靈活回收,若無可回收,則新建線程。
        //newCachedThreadPool的線程池數量爲無限大,可能會創建數量非常多的線程,甚至OOM
        ExecutorService threadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10 ; i++) {
            int finalI = i;
            threadPool.execute(()->{
                System.out.println(Thread.currentThread().getName()+"打印輸出i:"+ finalI);
            });
        }
        threadPool.shutdown();
    }

打印結果,可以看到,完成任務的線程會被重複利用

pool-1-thread-1打印輸出i:0
pool-1-thread-2打印輸出i:1
pool-1-thread-3打印輸出i:2
pool-1-thread-4打印輸出i:3
pool-1-thread-7打印輸出i:6
pool-1-thread-7打印輸出i:9
pool-1-thread-8打印輸出i:7
pool-1-thread-2打印輸出i:8
pool-1-thread-5打印輸出i:4
pool-1-thread-6打印輸出i:5

**弊端:**newCachedThreadPool的線程池數量爲無限大,可能會創建數量非常多的線程,甚至OOM

newFixedThreadPool

創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。

   private static void testNewFixedThreadPool() {
        //創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待
        //堆積的請求處理隊列可能會耗費非常大的內存,甚至OOM
        ExecutorService threadPool = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 10 ; i++) {
            int finalI = i;
            threadPool.execute(()->{
                System.out.println(Thread.currentThread().getName()+"打印輸出i:"+ finalI);
            });
        }
        threadPool.shutdown();
    }

可以看到,設置長度爲2的情況下,任務會等待完成後繼續執行

pool-1-thread-1打印輸出i:0
pool-1-thread-2打印輸出i:1
pool-1-thread-1打印輸出i:2
pool-1-thread-2打印輸出i:3
pool-1-thread-1打印輸出i:4
pool-1-thread-2打印輸出i:5
pool-1-thread-1打印輸出i:6
pool-1-thread-2打印輸出i:7
pool-1-thread-1打印輸出i:8
pool-1-thread-2打印輸出i:9

newScheduledThreadPool

newScheduledThreadPool主要用於支持定時及週期性任務執行。線程池長度可指定(適合一些簡單的定時或週期性任務,一般任務模塊的穩定性實現可以使用xxl_job | quartz)

延遲執行

    private static void testNewScheduledThreadPool() throws InterruptedException {
        //創建一個定長線程池,支持定時及週期性任務執行。
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
//        // 延遲執行 以下語句延遲3秒再打印
        scheduledThreadPool.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("this print delay 3 seconds");
            }
        }, 3, TimeUnit.SECONDS);
    }

週期執行

 private static void testNewScheduledThreadPool() throws InterruptedException {
        // 週期定時執行,延遲1秒後每3秒執行一次
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                logger.info("delay 1 seconds, and excute every 3 seconds");
            }
        }, 1, 3, TimeUnit.SECONDS);
    }

輸出結果如下

2022-03-04 16:30:50,805 [study.thread.ThreadAnalysis]-[INFO] delay 1 seconds, and excute every 3 seconds
2022-03-04 16:30:53,805 [study.thread.ThreadAnalysis]-[INFO] delay 1 seconds, and excute every 3 seconds
2022-03-04 16:30:56,803 [study.thread.ThreadAnalysis]-[INFO] delay 1 seconds, and excute every 3 seconds
2022-03-04 16:30:59,804 [study.thread.ThreadAnalysis]-[INFO] delay 1 seconds, and excute every 3 seconds

Executors提供的四種線程池雖各有優缺點,但總體而言不是最科學的線程池管理方式,在阿里發佈的編程規約中明確有以下說明:

線程池不允許使用Executors去創建,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。 說明:Executors各個方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
  主要問題是堆積的請求處理隊列可能會耗費非常大的內存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool:
  主要問題是線程數最大數是Integer.MAX_VALUE,可能會創建數量非常多的線程,甚至OOM。

我們閱讀Executors四種線程池底層源碼可以發現底層其實也是hreadPoolExecutor來創建的,推薦用ThreadPoolExecutor來構建管理線程池


3、最科學的線程池創建方式ThreadPoolExecutor

講ThreadPoolExecutor首先要分析它的構造函數,**構造函數和他的參數理解了,java線程池也就玩明白了

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler
)


工作策略如下:

 

接下來我們實際構造一個ThreadPoolExecutor線程池,分析其各個參數的含義

 private static void testThreadPoolExecutor(){
        //創建工作隊列,ArrayBlockingQueue限定隊列大小,指定容量(線程池和隊列都滿了後執行拒絕策略)
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(5);
//        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(5);
        ExecutorService threadPool = new ThreadPoolExecutor(
                // 核心線程數,如果運行線程數小於corePoolSize,提交新任務時就會新建一個線程來運行
                //如果運行線程數大於或等於corePoolSize,新提交的任務就會入列等待;
                2,
                // 最大線程數
                //如果等待隊列已滿,並且運行線程數小於maximumPoolSize,也將會新建一個線程來運行;
                //如果線程數大於maximumPoolSize,新提交的任務將會根據拒絕策略(RejectedExecutionHandler handler)來處理
                4,
                // keepAliveTime爲超過corePoolSize線程數量的線程指定最大空閒時間,unit爲時間單位
                60L, TimeUnit.SECONDS,
                //隊列,用於存放提交的等待執行任務
                // 入隊策略分爲三種:有界隊列(eg:ArrayBlockingQueue),無界隊列(eg:LinkedBlockingQueue),直接傳遞(eg:SynchronousQueue)
                workQueue,
                // 線程工廠,我們還可以自定義線程工廠來設置線程信息,如給線程統一設置前綴改名
                Executors.defaultThreadFactory(),
                // 指定拒絕策略
                // 拒絕策略默認提供四種,比較溫和的策略:AbortPolicy(在需要拒絕任務時拋出RejectedExecutionException),可自定義實現拒絕策略
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 10 ; i++) {
            int finalI = i;
            threadPool.execute(()->{
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"打印輸出i:"+ finalI);
            });
        }
        threadPool.shutdown();
    }

打印結果如下,可以看到,在最大線程數量maximumPoolSize限制爲4,且有界隊列ArrayBlockingQueue容

量只爲5時,執行10個線程任務,線程池達到飽和,有一個線程即因失敗拋出了RejectedExecutionException異常

Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task study.thread.ThreadAnalysis$$Lambda$1/990398217@614ddd49 rejected from java.util.concurrent.ThreadPoolExecutor@1c3a4799[Running, pool size = 4, active threads = 4, queued tasks = 5, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
	at study.thread.ThreadAnalysis.testThreadPoolExecutor(ThreadAnalysis.java:128)
	at study.thread.ThreadAnalysis.main(ThreadAnalysis.java:27)
pool-1-thread-1打印輸出i:0
pool-1-thread-3打印輸出i:7
pool-1-thread-4打印輸出i:8
pool-1-thread-2打印輸出i:1
pool-1-thread-4打印輸出i:4
pool-1-thread-2打印輸出i:5
pool-1-thread-1打印輸出i:2
pool-1-thread-3打印輸出i:3
pool-1-thread-4打印輸出i:6

關於隊列和拒絕策略的幾種不同形式要注意區分

默認三種不同的BlockingQueue隊列

newCachedThreadPool使用的是SynchronousQueue(我簡單理解,線程無限大也就不需要隊列,直接傳遞)
newSingleThreadExecutor和newFixedThreadPool使用的是LinkedBlockingQueue(除非系統資源耗盡,否則無界的任務隊列不存在任務入隊失敗)
newScheduledThreadPool使用的就是自定義實現的DelayedWorkQueue

**     這裏要注意使用無界隊列LinkedBlockingQueue時,maximumPoolSize就失去了作用**

4種不同的拒絕策略

可以看出AbortPolicy策略比較溫和,會拋出異常,爲系統穩定性考慮建議默認使用這個


4、怎麼基於自己的機器合理的設置線程?

線程池介紹完了,那麼最後我們如何根據項目的不同情況和機器配置來設置最優線程池呢?

一般來說有兩種衡量方式:

CPU密集型:corePoolSize = CPU核數 + 1 (eg:線程大多去執行一些計算任務之所以 +1 是考慮充分利用 cpu 資源, 避免空閒

IO密集型:corePoolSize = CPU核數 * 2 (eg: 線程大多去執行數據庫寫操作、發消息等因爲io密集型瓶頸不在cpu, 所以可以多開些線程

cpu核數計算

// 打印出本機可用cpu核數
System.out.println(Runtime.getRuntime().availableProcessors());

linux服務器的cpu核數計算(考慮到物理cpu和超線程技術,一般以邏輯cpu核數爲準)

1.核數和邏輯CPU計算公式
核數 = 物理CPU個數 * 每顆物理CPU的核數
邏輯CPU數 = 物理CPU個數 * 每顆物理CPU的核數 * 超線程數

(1)查看物理CPU個數
# grep "physical id" /proc/cpuinfo | sort | uniq| wc -l
(2)查看每個物理CPU中core的個數(即核數)
# grep "cpu cores" /proc/cpuinfo | uniq
(3)查看邏輯CPU的個數,推薦使用
# grep "processor" /proc/cpuinfo | wc -l

示例

最後來個實際項目的線程池配置示例吧

linux服務器的可用邏輯cpu核數爲16個(IO密集型任務,coreSize=cpu核數*2),我的線程池創建策略如下:

 @Bean
    ExecutorService executorService(){
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(128);
        return new ThreadPoolExecutor(32, 64, 60L, TimeUnit.SECONDS,
                workQueue, Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
    }

參考鏈接

Java 四種線程池newCachedThreadPool,newFixedThreadPool,newScheduledThreadPool,newSingleThreadExecutor

線程池不允許使用Executors去創建,而是通過ThreadPoolExecutor的方式

有界、無界隊列對ThreadPoolExcutor執行的影響

常見linux服務器物理CPU個數邏輯CPU個數計算方式

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