Spring Boot之多線程、異步:@Async

前言

來(lái)啦老鐵!

筆者學(xué)習(xí)Spring Boot有一段時(shí)間了,附上Spring Boot系列學(xué)習(xí)文章,歡迎取閱、賜教:

  1. 5分鐘入手Spring Boot;
  2. Spring Boot數(shù)據(jù)庫(kù)交互之Spring Data JPA;
  3. Spring Boot數(shù)據(jù)庫(kù)交互之Mybatis;
  4. Spring Boot視圖技術(shù);
  5. Spring Boot之整合Swagger;
  6. Spring Boot之junit單元測(cè)試踩坑;
  7. 如何在Spring Boot中使用TestNG;
  8. Spring Boot之整合logback日志;
  9. Spring Boot之整合Spring Batch:批處理與任務(wù)調(diào)度;
  10. Spring Boot之整合Spring Security: 訪問(wèn)認(rèn)證;
  11. Spring Boot之整合Spring Security: 授權(quán)管理;
  12. Spring Boot之多數(shù)據(jù)庫(kù)源:極簡(jiǎn)方案;
  13. Spring Boot之使用MongoDB數(shù)據(jù)庫(kù)源;

近期項(xiàng)目忙碌,家里事情也接踵而至,今天咱簡(jiǎn)單學(xué)點(diǎn)Spring Boot知識(shí):

  • Spring Boot之多線程、異步:@Async

通常情況下,我們基于Spring Boot寫(xiě)的API或方法,都是同步類型的,同步過(guò)程是阻塞式的,前一行代碼在未得到結(jié)果之前,會(huì)產(chǎn)生阻塞,后續(xù)的代碼就只能等待。比如,調(diào)用一個(gè)API,該API與數(shù)據(jù)庫(kù)交互,然后返回API結(jié)果,數(shù)據(jù)庫(kù)交互如果用了2秒鐘,那么返回API結(jié)果這個(gè)過(guò)程就要等2秒鐘,才能發(fā)生;

而實(shí)際場(chǎng)景中,有些功能其實(shí)不需要等待結(jié)果就可以執(zhí)行后續(xù)代碼,比如:

  • 發(fā)郵件功能,這個(gè)功能往往不用關(guān)心整個(gè)過(guò)程花了多久時(shí)間,只要最終對(duì)方能夠收到郵件即可,因此完全不用等待發(fā)郵件服務(wù)返回,可采用異步方式;
  • 一些任務(wù),如批處理任務(wù)等,觸發(fā)時(shí)往往只需要得到服務(wù)器的應(yīng)答,而不用等到任務(wù)執(zhí)行結(jié)束才告訴客戶端,也可采用異步的方式;
  • 等;

通常,我們采用多線程技術(shù)來(lái)實(shí)現(xiàn)異步過(guò)程,而Spring Boot中,對(duì)這個(gè)過(guò)程又做了簡(jiǎn)化,使用起來(lái)非常簡(jiǎn)單,接下來(lái)我們就一起來(lái)探索一下!

項(xiàng)目代碼已上傳Git Hub倉(cāng)庫(kù),歡迎取閱:

整體步驟

  1. 快速建立Spring Boot項(xiàng)目;
  2. 修飾項(xiàng)目啟動(dòng)類;
  3. 編寫(xiě)Service;
  4. 編寫(xiě)Controller;
  5. 驗(yàn)證效果;
  6. 線程池管理配置類;

1. 快速建立Spring Boot項(xiàng)目;

請(qǐng)參考5分鐘入手Spring Boot;

2. 修飾項(xiàng)目啟動(dòng)類;

在項(xiàng)目啟動(dòng)類上添加注解@EnableAsync即可:

package com.github.dylanz666;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

/**
 * @author : dylanz
 * @since : 09/27/2020
 */
@SpringBootApplication
@EnableAsync
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

3. 編寫(xiě)Service;

為了演示同步與異步的差異,以及異步的不同用法,我會(huì)編寫(xiě)3個(gè)service類和3個(gè)controller類;

1). 同步service類;

package com.github.dylanz666.service;

import org.springframework.stereotype.Service;

import java.util.Date;

/**
 * @author : dylanz
 * @since : 09/27/2020
 */
@Service
public class SyncTaskService {
    public void syncTask1() throws InterruptedException {
        Thread.sleep(2000);//模擬阻塞操作
        System.out.println(new Date() + ": syncTask1 complete.");
    }

    public void syncTask2() throws InterruptedException {
        Thread.sleep(2000);//模擬阻塞操作
        System.out.println(new Date() + ": syncTask2 complete.");
    }

    public void syncTask3() throws InterruptedException {
        Thread.sleep(2000);//模擬阻塞操作
        System.out.println(new Date() + ": syncTask3 complete.");
    }
}

2). 簡(jiǎn)單的異步service類;

package com.github.dylanz666.service;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.Date;

/**
 * @author : dylanz
 * @since : 09/27/2020
 */
@Service
public class AsyncTaskService {
    @Async
    public void asyncTask1() throws InterruptedException {
        Thread.sleep(2000);//模擬阻塞操作
        System.out.println(new Date() + ": asyncTask1 complete.");
    }

    @Async
    public void asyncTask2() throws InterruptedException {
        Thread.sleep(2000);//模擬阻塞操作
        System.out.println(new Date() + ": asyncTask2 complete.");
    }

    @Async
    public void asyncTask3() throws InterruptedException {
        Thread.sleep(2000);//模擬阻塞操作
        System.out.println(new Date() + ": asyncTask3 complete.");
    }
}

3). 異步拓展應(yīng)用的service類;

package com.github.dylanz666.service;

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.concurrent.Future;

/**
 * @author : dylanz
 * @since : 09/27/2020
 */
@Service
@Async
public class AsyncTaskService2 {
    public Future<String> asyncTask1() throws InterruptedException {
        Thread.sleep(10000);//模擬阻塞操作
        System.out.println(new Date() + ": asyncTask1 complete");
        return new AsyncResult<String>("asyncTask1 complete");
    }

    public Future<String> asyncTask2() throws InterruptedException {
        Thread.sleep(10000);//模擬阻塞操作
        System.out.println(new Date() + ": asyncTask1 complete");
        return new AsyncResult<String>("asyncTask1 complete");
    }

    public Future<String> asyncTask3() throws InterruptedException {
        Thread.sleep(10000);//模擬阻塞操作
        System.out.println(new Date() + ": asyncTask1 complete");
        return new AsyncResult<String>("asyncTask1 complete");
    }
}

簡(jiǎn)單解讀一下:

  • 要使一個(gè)方法或類稱為異步方法或類,只需要在方法或類上添加@Async即可,非常簡(jiǎn)單!
  • 可以通過(guò)方法返回Future類型(也可以用ListenableFuture)的對(duì)象,用于操作異步方法、提供異步方法的執(zhí)行狀態(tài)等;

4. 編寫(xiě)Controller;

1). 用于演示同步過(guò)程的API;

package com.github.dylanz666.controller;

import com.github.dylanz666.service.AsyncTaskService;
import com.github.dylanz666.service.SyncTaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

/**
 * @author : dylanz
 * @since : 09/27/2020
 */
@RestController
public class SyncTaskController {
    @Autowired
    private SyncTaskService syncTaskService;

    @GetMapping("/sync/task")
    @ResponseBody
    public String execute() throws InterruptedException {
        long startTimeStamp = System.currentTimeMillis();
        syncTaskService.syncTask1();
        syncTaskService.syncTask2();
        syncTaskService.syncTask3();
        long endTimeStamp = System.currentTimeMillis();
        String message = "sync tasks are complete, duration: " + (endTimeStamp - startTimeStamp) + " ms";
        System.out.println(message);
        return message;
    }
}

2). 用于演示簡(jiǎn)單異步過(guò)程的API;

package com.github.dylanz666.controller;

import com.github.dylanz666.service.AsyncTaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

/**
 * @author : dylanz
 * @since : 09/27/2020
 */
@RestController
public class AsyncTaskController {
    @Autowired
    private AsyncTaskService asyncTaskService;

    @GetMapping("/async/task")
    @ResponseBody
    public String execute() throws InterruptedException {
        long startTimeStamp = System.currentTimeMillis();
        asyncTaskService.asyncTask1();
        asyncTaskService.asyncTask2();
        asyncTaskService.asyncTask3();
        long endTimeStamp = System.currentTimeMillis();
        String message = "async tasks are triggered successfully, duration: " + (endTimeStamp - startTimeStamp) + " ms";
        System.out.println(message);
        return message;
    }
}

3). 用于演示異步拓展應(yīng)用的API;

package com.github.dylanz666.controller;

import com.github.dylanz666.service.AsyncTaskService2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.concurrent.Future;

/**
 * @author : dylanz
 * @since : 09/27/2020
 */
@RestController
@RequestMapping("/async/complex")
public class AsyncTaskController2 {
    @Autowired
    private AsyncTaskService2 asyncTaskService2;

    public static String status = "async tasks are not triggered.";
    public static Future<String> task1;
    public static Future<String> task2;
    public static Future<String> task3;

    @GetMapping("/task")
    @ResponseBody
    public String execute() throws InterruptedException {
        long startTimeStamp = System.currentTimeMillis();
        task1 = asyncTaskService2.asyncTask1();
        task2 = asyncTaskService2.asyncTask2();
        task3 = asyncTaskService2.asyncTask3();
        long endTimeStamp = System.currentTimeMillis();
        status = "async tasks are doing.";
        String message = "async tasks are triggered successfully, duration: " + (endTimeStamp - startTimeStamp) + " ms";
        System.out.println(message);
        return message;
    }

    @GetMapping("/task/status")
    @ResponseBody
    public String getTasksStatus() {
        assert task1 != null;
        if (task1.isDone() && task2.isDone() && task3.isDone()) {
            status = "async tasks are done.";
        }
        return status;
    }

    @GetMapping("/task/status/{taskId}")
    @ResponseBody
    public Boolean getTaskStatus(@PathVariable(name = "taskId") int taskId) {
        boolean taskStatus = false;
        switch (taskId) {
            case 1:
                taskStatus = task1.isDone();
                break;
            case 2:
                taskStatus = task2.isDone();
                break;
            case 3:
                taskStatus = task3.isDone();
                break;
        }
        return taskStatus;
    }
}

項(xiàng)目整體結(jié)構(gòu)如下:

項(xiàng)目整體結(jié)構(gòu)

5. 驗(yàn)證效果;

啟動(dòng)項(xiàng)目:
啟動(dòng)項(xiàng)目

1). 驗(yàn)證同步API和執(zhí)行結(jié)果;

瀏覽器直接訪問(wèn) http://127.0.0.1:8080/sync/task

同步API
同步API log
解讀:
  • 由于syncTask1、syncTask2、syncTask3各等待了2秒鐘,最終整體用時(shí)6.002秒才返回API結(jié)果;
  • syncTask1、syncTask2、syncTask3按代碼中指定的順序,順序執(zhí)行;

2). 驗(yàn)證簡(jiǎn)單異步API和執(zhí)行結(jié)果;

瀏覽器直接訪問(wèn) http://127.0.0.1:8080/async/task

簡(jiǎn)單異步API
簡(jiǎn)單異步API log
解讀:
  • 雖然asyncTask1、asyncTask2、asyncTask3的代碼中也寫(xiě)了等待2秒的代碼,但API調(diào)用時(shí)卻未等待,而是直接返回結(jié)果,僅用了4毫秒,速度非常快;
  • asyncTask1、asyncTask2、asyncTask3三個(gè)方法均采用@Async注解,三者并行執(zhí)行,即使用了3個(gè)線程,基本無(wú)先后概念;
3). 驗(yàn)證異步拓展應(yīng)用API和執(zhí)行結(jié)果;

這里頭的API主要演示@Async可以寫(xiě)在類上,可以通過(guò)返回Future類型的對(duì)象,對(duì)異步任務(wù)進(jìn)行處理和獲取其信息;

瀏覽器直接訪問(wèn) http://127.0.0.1:8080/async/complex/task

調(diào)用異步擴(kuò)展應(yīng)用API
調(diào)用異步擴(kuò)展應(yīng)用API log
前10秒內(nèi) task狀態(tài)
前10秒內(nèi) 單個(gè)task狀態(tài)
10秒后 task狀態(tài)
10秒后 單個(gè)task狀態(tài)

這樣的拓展,我們不僅會(huì)使用多線程、異步,而且能夠獲取異步方法的狀態(tài),真香!

6. 線程池管理配置類;

上述這種方式,當(dāng)并發(fā)量很小時(shí),上述方式一般不會(huì)有問(wèn)題,但當(dāng)并發(fā)量很大時(shí),可能會(huì)遇到一些問(wèn)題:
  • 由于我們沒(méi)有對(duì)線程數(shù)量進(jìn)行限制,如果所有請(qǐng)求都去占用資源,很容易使資源負(fù)載過(guò)大,甚至宕機(jī);
  • 當(dāng)我們需要的并發(fā)執(zhí)行線程數(shù)量很多時(shí),且每個(gè)線程執(zhí)行很短的時(shí)間就結(jié)束了,這樣,我們頻繁的創(chuàng)建、銷毀線程就大大降低了工作效率(創(chuàng)建和銷毀線程需要時(shí)間、資源);
  • 線程池可以使一個(gè)線程執(zhí)行完任務(wù)之后,繼續(xù)去執(zhí)行下一個(gè)任務(wù),不被銷毀,這樣線程利用率就提高了。
因此,有必要使用線程池對(duì)線程進(jìn)行管理。

通常我們使用Spring提供的ThreadPoolTaskExecutor,進(jìn)行線程管理;
項(xiàng)目?jī)?nèi)創(chuàng)建config包,并建立配置類ThreadPoolConfig(名字隨意),代碼如下:

package com.github.dylanz666.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author : dylanz
 * @since : 09/27/2020
 */
@Configuration
@EnableAsync
public class ThreadPoolConfig {
    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //設(shè)置核心線程數(shù)
        executor.setCorePoolSize(5);
        //設(shè)置最大線程數(shù)
        executor.setMaxPoolSize(10);
        //設(shè)置隊(duì)列容量
        executor.setQueueCapacity(20);
        //設(shè)置線程活躍時(shí)間(秒)
        executor.setKeepAliveSeconds(60);
        //設(shè)置默認(rèn)線程名稱
        executor.setThreadNamePrefix("demo-");
        //設(shè)置拒絕策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //等待所有任務(wù)結(jié)束后再關(guān)閉線程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        return executor;
    }
}

如果采用配置類方式管理線程,則項(xiàng)目入口類的@EnableAsync可以去除;
此時(shí)項(xiàng)目整體結(jié)構(gòu):

項(xiàng)目整體結(jié)構(gòu)

至此,我們學(xué)會(huì)了Spring Boot多線程、異步的基本使用方法,非常簡(jiǎn)單,相信未來(lái)定能派上用場(chǎng)?。。?/h3>

如果本文對(duì)您有幫助,麻煩點(diǎn)贊+關(guān)注!

謝謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容