使用SpringBoot的線程池處理異步任務

這是我參與更文挑戰的第3天,活動詳情查看: 更文挑戰html

介紹

最近在作項目時瞭解了最好不要直接使用 new Thread(...).start() ,用線程池來隱式的維護全部線程,具體爲何能夠看這篇文章java

其實 SpringBoot 已經爲咱們建立並配置好了這個東西,這裏就來學習一下如何來使用 SpringBoot 爲咱們設置的線程池。git

若有錯誤歡迎聯繫我指正!es6

使用

建立配置類

首先咱們須要建立一個配置類來讓 SpringBoot 加載,而且在裏面設置一些本身須要的參數。spring

@Configuration
@EnableAsync
public class ExecutorConfig {

    private static final Logger logger = LoggerFactory.getLogger(ExecutorConfig.class);

    @Value("${async.executor.thread.core_pool_size}")
    private int corePoolSize;
    @Value("${async.executor.thread.max_pool_size}")
    private int maxPoolSize;
    @Value("${async.executor.thread.queue_capacity}")
    private int queueCapacity;
    @Value("${async.executor.thread.keep_alive_seconds}")
    private int keepAliveSeconds;
    @Value("${async.executor.thread.name.prefix}")
    private String namePrefix;

    @Bean(name = "asyncServiceExecutor")
    public Executor asyncServiceExecutor() {
        logger.info("開啓SpringBoot的線程池!");

        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        // 設置核心線程數
        executor.setCorePoolSize(corePoolSize);
        // 設置最大線程數
        executor.setMaxPoolSize(maxPoolSize);
        // 設置緩衝隊列大小
        executor.setQueueCapacity(queueCapacity);
        // 設置線程的最大空閒時間
        executor.setKeepAliveSeconds(keepAliveSeconds);
        // 設置線程名字的前綴
        executor.setThreadNamePrefix(namePrefix);
        // 設置拒絕策略:當線程池達到最大線程數時,如何處理新任務
        // CALLER_RUNS:在添加到線程池失敗時會由主線程本身來執行這個任務
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

        // 線程池初始化
        executor.initialize();

        return executor;
    }
}
複製代碼

首先,api

  • @Configuration 的做用是代表這是一個配置類。
  • @EnableAsync 的做用是啓用 SpringBoot 的異步執行

其次,關於線程池的設置有markdown

  • corePoolSize: 核心線程數,當向線程池提交一個任務時池裏的線程數小於核心線程數,那麼它會建立一個線程來執行這個任務,一直直到池內的線程數等於核心線程數
  • maxPoolSize: 最大線程數,線程池中容許的最大線程數量。關於這兩個數量的區別我會在下面解釋
  • queueCapacity: 緩衝隊列大小,用來保存阻塞任務隊列(注意這裏的隊列放的是任務而不是線程)
  • keepAliveSeconds: 容許線程存活時間(空閒狀態下),單位爲秒,默認60s
  • namePrefix: 線程名前綴
  • RejectedExecutionHandler: 拒絕策略,當線程池達到最大線程數時,如何處理新任務。線程池爲咱們提供的策略有
    • AbortPolicy:默認策略。直接拋出 RejectedExecutionException
    • DiscardPolicy:直接丟棄掉被拒絕的任務,且不會拋出任何異常
    • DiscardOldestPolicy:丟棄掉隊列中的隊頭元素(也就是最先在隊列裏的任務),而後從新執行 提交該任務 的操做
    • CallerRunsPolicy:由主線程本身來執行這個任務,該機制將減慢新任務的提交

關於 corePoolSizemaxPoolSize 的區別也是困惑了我好久,官方文檔上的解釋說的很清楚。個人理解以下:oracle

這個線程池實際上是有點「彈性的」。當向線程池提交任務時:app

  • 當前運行的線程數 < corePoolSize異步

    即便其它的工做線程處於空閒狀態,線程池也會建立一個新線程來執行任務

  • corePoolSize < 當前運行的線程數 < maxPoolSize

    • 隊列已滿

      則 建立新線程來執行任務

    • 隊列未滿

      則 加入隊列中

  • 當前運行的線程數 > maxPoolSize

    • 隊列已滿

      則 拒絕任務

    • 隊列未滿

      則 加入隊列中

因此當想要建立固定大小的線程池時,將 corePoolSizemaxPoolSize 設置成同樣就好了。

最後,別忘了給方法加上 @Bean 註解,不然 SpringBoot 不會加載。

這裏由於我加了 @Value 註解,能夠在 application.properties 中配置相關數據,如

# 配置核心線程數
async.executor.thread.core_pool_size = 5
# 配置最大線程數
async.executor.thread.max_pool_size = 5
# 配置隊列大小
async.executor.thread.queue_capacity = 999
# 配置線程池中的線程的名稱前綴
async.executor.thread.name.prefix = test-async-
# 配置線程最大空閒時間
async.executor.thread.keep_alive_seconds = 30
複製代碼

在具體的方法中使用

配置完上面那些使用起來就輕鬆了,只需在業務方法前加上 @Async 註解,它就會異步執行了。

在 Service 中添加以下方法

@Async("asyncServiceExecutor")
// 注:@Async所修飾的函數不能定義爲static類型,這樣異步調用不會生效
public void asyncTest() throws InterruptedException {
    logger.info("任務開始!");

    System.out.println("異步執行某耗時的事...");
    System.out.println("如休眠5秒");
    Thread.sleep(5000);

    logger.info("任務結束!");
}
複製代碼

而後在 Controller 裏調用一下這個方法,在網頁上連續發送請求作一個測試。 我這裏連續發起了5次請求,能夠看到這5個任務確實是成功地異步執行了。

res1.png

我設置的線程池大小爲 5,因此當超過 5 個任務被提交時,會放入阻塞隊列中

res2.png

到這裏,基本的異步執行任務就實現了。

自定義

雖然它提供給咱們的線程池已經很強大了,可是有時候咱們還須要一些額外信息,好比說咱們想知道這個線程池已經執行了多少任務了、當前有多少線程在運行、阻塞隊列裏還有多少任務等等。那麼這個時候咱們就能夠自定義咱們的線程池。

自定義很簡單,本身寫一個類繼承 Spring 提供的 ThreadPoolTaskExecutor,在此之上作修改就行了。如

public class VisibleThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {

    private static final Logger logger = LoggerFactory.getLogger(VisibleThreadPoolTaskExecutor.class);

    public void info() {
        ThreadPoolExecutor executor = getThreadPoolExecutor();

        if (executor == null) return;

        String info = "線程池" + this.getThreadNamePrefix() +
                "中,總任務數爲 " + executor.getTaskCount() +
                " ,已處理完的任務數爲 " + executor.getCompletedTaskCount() +
                " ,目前正在處理的任務數爲 " + executor.getActiveCount() +
                " ,緩衝隊列中任務數爲 " + executor.getQueue().size();

        logger.info(info);
    }

    @Override
    public void execute(Runnable task) {
        info();
        super.execute(task);
    }

    @Override
    public void execute(Runnable task, long startTimeout) {
        info();
        super.execute(task, startTimeout);
    }

    @Override
    public Future<?> submit(Runnable task) {
        info();
        return super.submit(task);
    }

    @Override
    public <T> Future<T> submit(Callable<T> task) {
        info();
        return super.submit(task);
    }

    @Override
    public ListenableFuture<?> submitListenable(Runnable task) {
        info();
        return super.submitListenable(task);
    }

    @Override
    public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
        info();
        return super.submitListenable(task);
    }
}
複製代碼

而後在咱們的配置類 ExecutorConfig 中將

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 改成 ThreadPoolTaskExecutor executor = new VisibleThreadPoolTaskExecutor();, 也就是使用咱們本身定義的線程池,而後會在相應的任務執行(execute())、任務提交(submit())時打印咱們須要的信息了。

打印結果,在此以前已處理了5個任務:

res3.png

查詢線程池信息

上面自定義線程池後想查詢信息只能在線程池中的方法查詢,那若是我想在任意地方查詢線程池的信息呢?那也是能夠的,並且很是簡單。我這裏寫一個接口來查詢線程池的任務信息以作示例。

首先修改一下線程池裏的 Info() 方法,讓它返回咱們須要的信息。

public String info() {
    ThreadPoolExecutor executor = getThreadPoolExecutor();
    if (executor == null) return "線程池不存在";

    String info = "線程池" + this.getThreadNamePrefix() +
            "中,總任務數爲 " + executor.getTaskCount() +
            " ,已處理完的任務數爲 " + executor.getCompletedTaskCount() +
            " ,目前正在處理的任務數爲 " + executor.getActiveCount() +
            " ,緩衝隊列中任務數爲 " + executor.getQueue().size();

    logger.info(info);

    return info;
}
複製代碼

而後修改一下配置類 ExecutorConfig 裏註冊線程池的方法,讓它註冊的是咱們自定義的線程池類型

@Bean(name = "asyncServiceExecutor")
public VisibleThreadPoolTaskExecutor asyncServiceExecutor() {
    logger.info("開啓SpringBoot的線程池!");

    // 修改這裏,要返回咱們本身定義的類 VisibleThreadPoolTaskExecutor
    VisibleThreadPoolTaskExecutor executor = new VisibleThreadPoolTaskExecutor();
// ThreadPoolTaskExecutor executor = new VisibleThreadPoolTaskExecutor();

    // 設置核心線程數
    executor.setCorePoolSize(corePoolSize);
    // 設置最大線程數
    executor.setMaxPoolSize(maxPoolSize);
    // 設置緩衝隊列大小
    executor.setQueueCapacity(queueCapacity);
    // 設置線程的最大空閒時間
    executor.setKeepAliveSeconds(keepAliveSeconds);
    // 設置線程名字的前綴
    executor.setThreadNamePrefix(namePrefix);
    // 設置拒絕策略:當線程池達到最大線程數時,如何處理新任務
    // CALLER_RUNS:不在新線程中執行任務,而是有調用者所在的線程來執行
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

    // 線程池初始化
    executor.initialize();

    return executor;
}
複製代碼

再在咱們須要信息的地方自動注入這個線程池,而後調用一下 info() 方法就能獲得信息了,我這裏以在 Service 層中獲取信息爲例。

@Service
public class DemoService {

    private static final Logger logger = LoggerFactory.getLogger(DemoService.class);

    // 別忘了這裏要用 SpringBoot 的自動注入
    @Autowired
    private VisibleThreadPoolTaskExecutor executor;

    // @SneakyThrows 這個註解是Lombok帶的,我爲了代碼簡潔使用的。你也可使用 try catch 的方法。
    @SneakyThrows 
    @Async("asyncServiceExecutor")
    public void asyncTest() {
        logger.info("任務開始!");

        System.out.println("異步執行某耗時的事...");
        System.out.println("如休眠5秒");
        Thread.sleep(5000);

        logger.info("任務結束!");

        // 你甚至能夠在任務結束時再打印一下線程池信息
        executor.info();
    }

    public String getExecutorInfo() {
        return executor.info();
    }
}
複製代碼

最後在 Controller 層中調用一下,就大功告成了!

@RestController
public class DemoController {

    @Autowired
    private DemoService demoService;

    @GetMapping("/async")
    public void async() {
        demoService.asyncTest();
    }

    @GetMapping("/info")
    public String info() {
        return demoService.getExecutorInfo();
    }
}
複製代碼

來看一下測試的結果吧,我這裏調用 /async 一口氣開啓了 15 個任務,而後在不一樣時間使用 /info 來看看信息。

剛開始時的結果:

res4.png

一口氣提交了15個任務後的中間結果:

res5.png

全部任務都執行完了的最終結果:

res6.png

總結

本篇到這裏就結束了,篇幅略長。總結一下,要想在SpringBoot中使用它提供的線程池其實很簡單,只要兩步:

  1. 註冊線程池(使用 @Bean 來註冊),設置一些本身想要的參數
  2. 在想要異步調用的方法上加上 @Async 註解

固然你也能夠不使用 @Async 註解,直接在想開線程的地方自動注入你註冊的線程池,而後像普通線程池同樣使用就好了。

其實關於這一方面的知識也講得並不夠詳盡,好比線程池裏還有哪些方法、SpringBoot是如何爲咱們弄得這麼方便的等等,還須要多多補充知識。

參考

相關文章
相關標籤/搜索