Java線程池和SpringBoot異步線程池

目錄

一、SpringBoot異步線程池

1、定義線程池

2、線程池的使用

二、ThreadPoolTaskExecutor和ThreadPoolExecutor區別

1、ThreadPoolExecutor的處理流程 

2、四種Reject預定義策略

三、Java線程池

1、使用線程池的優勢

2、什麼是阻塞隊列?

3、線程池爲什麼要是使用阻塞隊列?

4、如何配置線程池?

5、Java中提供的線程池

(1)newCachedThreadPool

(2)newFixedThreadPool

(3)newSingleThreadExecutor

(4)newScheduledThreadPool


一、SpringBoot異步線程池

1、定義線程池

代碼示例:配置一個線程池,這裏使用spring封裝的線程池

@EnableAsync // 開啓異步任務
@Configuration
public class TaskPoolConfig {
    @Bean("taskExecutor") // 線程池名稱
    public Executor taskExecutor() {
        // 使用Spring封裝的異步線程池
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);               // 初始化線程數
        executor.setMaxPoolSize(20);                // 最大線程數
        executor.setQueueCapacity(200);             // 緩衝隊列
        executor.setKeepAliveSeconds(60);           // 允許空閒時間/秒
        executor.setThreadNamePrefix("taskExecutor-");// 線程池名前綴-方便日誌查找
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(60);
        executor.initialize();   // 初始化
        return executor;
    }
}

上面我們通過使用ThreadPoolTaskExecutor創建了一個線程池,同時設置了以下這些參數:

  • 核心線程數10:線程池創建時候初始化的線程數
  • 最大線程數20:線程池最大的線程數,只有在緩衝隊列滿了之後纔會申請超過核心線程數的線程
  • 緩衝隊列200:用來緩衝執行任務的隊列
  • 允許線程的空閒時間60秒:當超過了核心線程出之外的線程在空閒時間到達之後會被銷燬
  • 線程池名的前綴:設置好了之後可以方便我們定位處理任務所在的線程池
  • 線程池對拒絕任務的處理策略:這裏採用了CallerRunsPolicy策略,當線程池沒有處理能力的時候,該策略會直接在 execute 方法的調用線程中運行被拒絕的任務;如果執行程序已關閉,則會丟棄該任務

說明:setWaitForTasksToCompleteOnShutdown(true)該方法就是這裏的關鍵,用來設置線程池關閉的時候等待所有任務都完成再繼續銷燬其他的Bean,這樣這些異步任務的銷燬就會先於Redis線程池的銷燬。同時,這裏還設置了setAwaitTerminationSeconds(60),該方法用來設置線程池中任務的等待時間,如果超過這個時候還沒有銷燬就強制銷燬,以確保應用最後能夠被關閉,而不是阻塞住。

2、線程池的使用

使用多線程,往往是創建Thread,或者是實現runnable接口,用到線程池的時候還需要創建Executors,spring中有十分優秀的支持,就是註解@EnableAsync就可以使用多線程,@Async加在線程任務的方法上(需要異步執行的任務),定義一個線程任務,通過spring提供的ThreadPoolTaskExecutor就可以使用線程池。

線程池的使用在Spring中非常簡單,只要設置兩個註解就可以了

(1)@EnableAsync                   // 開啓異步任務

(2)@Async("taskExecutor")   // 申明爲異步方法,指定線程池名稱

注: @Async所修飾的函數不要定義爲static類型,這樣異步調用不會生效

@Slf4j
@Component
public class Task {

    public static Random random = new Random();

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Async("taskExecutor")
    public void doTaskOne() throws Exception {
        log.info("開始做任務一");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info(stringRedisTemplate.randomKey());
        log.info("完成任務一,耗時:" + (end - start) + "毫秒");
    }

    @Async("taskExecutor")
    public void doTaskTwo() throws Exception {
        log.info("開始做任務二");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任務二,耗時:" + (end - start) + "毫秒");
    }

    @Async("taskExecutor")
    public void doTaskThree() throws Exception {
        log.info("開始做任務三");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任務三,耗時:" + (end - start) + "毫秒");
    }

}

簡單測試下:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class TaskTest {
    @Autowired
    private Task task;
    @Test
    public void test() throws Exception {
        task.doTaskOne();
        task.doTaskTwo();
        task.doTaskThree();
        Thread.currentThread().join();
    }
}

測試結果如下:

2020-04-16 15:23:13.834  INFO 1828 --- [ taskExecutor-1] demo.spring.tasks.Task                   : 開始做任務一
2020-04-16 15:23:13.834  INFO 1828 --- [ taskExecutor-2] demo.spring.tasks.Task                   : 開始做任務二
2020-04-16 15:23:13.835  INFO 1828 --- [ taskExecutor-3] demo.spring.tasks.Task                   : 開始做任務三
2020-04-16 15:23:17.539  INFO 1828 --- [ taskExecutor-2] demo.spring.tasks.Task                   : 完成任務二,耗時:3704毫秒
2020-04-16 15:23:18.380  INFO 1828 --- [   scheduling-1] demo.spring.tasks.ScheduledTasks         : ScheduledTasks1 - The time is now 15:23:18
2020-04-16 15:23:18.381  INFO 1828 --- [   scheduling-1] demo.spring.tasks.ScheduledTasks         : ScheduledTasks2 - The time is now 15:23:18
2020-04-16 15:23:19.475  INFO 1828 --- [ taskExecutor-3] demo.spring.tasks.Task                   : 完成任務三,耗時:5640毫秒

其中任務一會報錯,因爲沒有配置相關的redis配置,但是並不影響其他任務的執行。

二、ThreadPoolTaskExecutor和ThreadPoolExecutor區別

ThreadPoolTaskExecutor是Spring對ThreadPoolExecutor進行封裝,它實現方式完全是使用threadPoolExecutor進行實現,來看一下源碼

public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport implements SchedulingTaskExecutor {
    private final Object poolSizeMonitor = new Object();
    private int corePoolSize = 1;
    private int maxPoolSize = 2147483647;
    private int keepAliveSeconds = 60;
    private boolean allowCoreThreadTimeOut = false;
    private int queueCapacity = 2147483647;
    private ThreadPoolExecutor threadPoolExecutor;   //這裏就用到了ThreadPoolExecutor

瞭解了ThreadPoolTaskExecutor的相關情況,接下來看一下ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  • int corePoolSize:線程池維護線程的最小數量.    
  • int maximumPoolSize:線程池維護線程的最大數量.    
  • long keepAliveTime:空閒線程的存活時間.    
  • TimeUnit unit: 時間單位,現有納秒,微秒,毫秒,秒枚舉值.    
  • BlockingQueue<Runnable> workQueue:持有等待執行的任務隊列. 
  • threadFactory(線程工廠):用於創建新線程。threadFactory創建的線程也是採用new Thread()方式,threadFactory創建的線程名都具有統一的風格:pool-m-thread-n(m爲線程池的編號,n爲線程池內的線程編號)  

  • RejectedExecutionHandler handler:  線程飽和策略,用來拒絕一個任務的執行.   

RejectedExecutionHandler handler:  用來拒絕一個任務的執行,有兩種情況會發生這種情況:

一是在execute方法中若addIfUnderMaximumPoolSize(command)爲false,即線程池已經飽和;    

二是在execute方法中, 發現runState!=RUNNING || poolSize == 0,即已經shutdown,就調用ensureQueuedTaskHandled(Runnable command),在該方法中有可能調用reject。

1、ThreadPoolExecutor的處理流程 

1)當池子大小小於corePoolSize就新建線程,並處理請求

2)當池子大小等於corePoolSize,把請求放入workQueue中,池子裏的空閒線程就去從workQueue中取任務並處理

3)當workQueue放不下新入的任務時,新建線程入池,並處理請求,如果池子大小撐到了maximumPoolSize就用RejectedExecutionHandler來做拒絕處理

4)另外,當池子的線程數大於corePoolSize的時候,多餘的線程會等待keepAliveTime長的時間,如果無請求可處理就自行銷燬

總結下:

ThreadPoolExecutor會優先創建  CorePoolSiz 線程, 當繼續增加線程時,先放入Queue中,當 CorePoolSiz  和 Queue 都滿的時候,就增加創建新線程,當線程達到MaxPoolSize的時候,就會拋出錯誤 org.springframework.core.task.TaskRejectedException

另外MaxPoolSize的設定如果比系統支持的線程數還要大時,會拋出java.lang.OutOfMemoryError: unable to create new native thread 異常。

2、四種Reject預定義策略

(1)ThreadPoolExecutor.AbortPolicy策略,是默認的策略,處理程序遭到拒絕將拋出運行時 RejectedExecutionException。 

(2)ThreadPoolExecutor.CallerRunsPolicy策略 ,調用者的線程會執行該任務,如果執行器已關閉,則丟棄。

(3)ThreadPoolExecutor.DiscardPolicy策略,不能執行的任務將被丟棄。

(4)ThreadPoolExecutor.DiscardOldestPolicy策略,如果執行程序尚未關閉,則位於工作隊列頭部的任務將被刪除,然後重試執行程序(如果再次失敗,則重複此過程)。

三、Java線程池

1、使用線程池的優勢

  1. 降低系統資源消耗,通過重用已存在的線程,降低線程創建和銷燬造成的消耗;
  2. 提高系統響應速度,當有任務到達時,通過複用已存在的線程,無需等待新線程的創建便能立即執行;
  3. 方便線程併發數的管控。因爲線程若是無限制的創建,可能會導致內存佔用過多而產生OOM,並且會造成cpu過度切換(cpu切換線程是有時間成本的(需要保持當前執行線程的現場,並恢復要執行線程的現場))。
  4. 提供更強大的功能,延時定時線程池。

2、什麼是阻塞隊列?

阻塞隊列BlockingQueue,相當我們經常接觸的List,但如果BlockQueue是空的,這時如果有線程要從這個BlockingQueue取元素的時候將會被阻塞進入等待狀態,直到別的線程在BlockingQueue中添加進了元素,被阻塞的線程纔會被喚醒。同樣,如果BlockingQueue是滿的,試圖往隊列中存放元素的線程也會被阻塞進入等待狀態,直到BlockingQueue裏的元素被別的線程拿走纔會被喚醒繼續操作。

3、線程池爲什麼要是使用阻塞隊列?

阻塞隊列可以保證任務隊列中沒有任務時阻塞獲取任務的線程,使得線程進入wait狀態,釋放cpu資源。當隊列中有任務時才喚醒對應線程從隊列中取出消息進行執行。使得線程不至於一直佔用cpu資源。

線程執行完任務後通過循環再次從任務隊列中取出任務進行執行,代碼片段如下while (task != null || (task = getTask()) != null) {...}。

不用阻塞隊列也是可以的,不過實現起來比較麻煩而已,有好用的爲啥不用呢?

4、如何配置線程池?

(1)CPU密集型任務
儘量使用較小的線程池,一般爲CPU核心數+1。 因爲CPU密集型任務使得CPU使用率很高,若開過多的線程數,會造成CPU過度切換。

(2)IO密集型任務
可以使用稍大的線程池,一般爲2*CPU核心數。 IO密集型任務CPU使用率並不高,因此可以讓CPU在等待IO的時候有其他線程去處理別的任務,充分利用CPU時間。

(3)混合型任務
可以將任務分成IO密集型和CPU密集型任務,然後分別用不同的線程池去處理。 只要分完之後兩個任務的執行時間相差不大,那麼就會比串行執行來的高效。
因爲如果劃分之後兩個任務執行時間有數據級的差距,那麼拆分沒有意義。
因爲先執行完的任務就要等後執行完的任務,最終的時間仍然取決於後執行完的任務,而且還要加上任務拆分與合併的開銷得不償失。

5、Java中提供的線程池

Executors類提供了4種不同的線程池:newCachedThreadPool,newFixedThreadPool,newScheduledThreadPool,newSingleThreadExecutor

(1)newCachedThreadPool

用來創建一個可以無限擴大的線程池,適用於負載較輕的場景,執行短期異步任務。(可以使得任務快速得到執行,因爲任務時間執行短,可以很快結束,也不會造成cpu過度切換)

(2)newFixedThreadPool

創建一個固定大小的線程池,因爲採用無界的阻塞隊列,所以實際線程數量永遠不會變化,適用於負載較重的場景,對當前線程數量進行限制。(保證線程數可控,不會造成線程過多,導致系統負載更爲嚴重)

(3)newSingleThreadExecutor

創建一個單線程的線程池,適用於需要保證順序執行各個任務

(4)newScheduledThreadPool

適用於執行延時或者週期性任務

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