Spring Boot之多線(xiàn)程、異步:@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: 訪(fǎng)問(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之多線(xiàn)程迅腔、異步:@Async

通常情況下,我們基于Spring Boot寫(xiě)的API或方法靠娱,都是同步類(lèi)型的沧烈,同步過(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é)束才告訴客戶(hù)端,也可采用異步的方式底靠;
  • 等害晦;

通常,我們采用多線(xiàn)程技術(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)類(lèi)殴俱;
  3. 編寫(xiě)Service政冻;
  4. 編寫(xiě)Controller;
  5. 驗(yàn)證效果线欲;
  6. 線(xiàn)程池管理配置類(lèi)明场;

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

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

2. 修飾項(xiàng)目啟動(dòng)類(lèi)李丰;

在項(xiàng)目啟動(dòng)類(lèi)上添加注解@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類(lèi)和3個(gè)controller類(lèi)舟舒;

1). 同步service類(lèi);

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類(lèi)嗜憔;

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類(lèi)秃励;

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è)方法或類(lèi)稱(chēng)為異步方法或類(lèi),只需要在方法或類(lèi)上添加@Async即可吉捶,非常簡(jiǎn)單夺鲜!
  • 可以通過(guò)方法返回Future類(lèi)型(也可以用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é)果;

瀏覽器直接訪(fǎng)問(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é)果;

瀏覽器直接訪(fǎng)問(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è)線(xiàn)程,基本無(wú)先后概念秋冰;
3). 驗(yàn)證異步拓展應(yīng)用API和執(zhí)行結(jié)果仲义;

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

瀏覽器直接訪(fǎng)問(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ì)使用多線(xiàn)程、異步苛谷,而且能夠獲取異步方法的狀態(tài)辅鲸,真香!

6. 線(xiàn)程池管理配置類(lèi)腹殿;

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

通常我們使用Spring提供的ThreadPoolTaskExecutor弟劲,進(jìn)行線(xiàn)程管理;
項(xiàng)目?jī)?nèi)創(chuàng)建config包唉地,并建立配置類(lèi)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è)置核心線(xiàn)程數(shù)
        executor.setCorePoolSize(5);
        //設(shè)置最大線(xiàn)程數(shù)
        executor.setMaxPoolSize(10);
        //設(shè)置隊(duì)列容量
        executor.setQueueCapacity(20);
        //設(shè)置線(xiàn)程活躍時(shí)間(秒)
        executor.setKeepAliveSeconds(60);
        //設(shè)置默認(rèn)線(xiàn)程名稱(chēng)
        executor.setThreadNamePrefix("demo-");
        //設(shè)置拒絕策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //等待所有任務(wù)結(jié)束后再關(guān)閉線(xiàn)程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        return executor;
    }
}

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

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

至此,我們學(xué)會(huì)了Spring Boot多線(xiàn)程朱盐、異步的基本使用方法群嗤,非常簡(jiǎn)單,相信未來(lái)定能派上用場(chǎng)1铡?衩亍!

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

謝謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末清女,一起剝皮案震驚了整個(gè)濱河市钱烟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌嫡丙,老刑警劉巖忠售,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異迄沫,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)卦方,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)羊瘩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事尘吗∈潘” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵睬捶,是天一觀(guān)的道長(zhǎng)黔宛。 經(jīng)常有香客問(wèn)我,道長(zhǎng)擒贸,這世上最難降的妖魔是什么臀晃? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮介劫,結(jié)果婚禮上徽惋,老公的妹妹穿的比我還像新娘。我一直安慰自己座韵,他們只是感情好险绘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著誉碴,像睡著了一般宦棺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上黔帕,一...
    開(kāi)封第一講書(shū)人閱讀 51,301評(píng)論 1 301
  • 那天代咸,我揣著相機(jī)與錄音,去河邊找鬼蹬屹。 笑死侣背,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的慨默。 我是一名探鬼主播贩耐,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼厦取!你這毒婦竟也來(lái)了潮太?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤虾攻,失蹤者是張志新(化名)和其女友劉穎铡买,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體霎箍,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡奇钞,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了漂坏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片景埃。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡媒至,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谷徙,到底是詐尸還是另有隱情拒啰,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布完慧,位于F島的核電站谋旦,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏屈尼。R本人自食惡果不足惜册着,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鸿染。 院中可真熱鬧指蚜,春花似錦、人聲如沸涨椒。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蚕冬。三九已至免猾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間囤热,已是汗流浹背猎提。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留旁蔼,地道東北人锨苏。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像棺聊,于是被迫代替她去往敵國(guó)和親伞租。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354