SpringBoot學習筆記(十七:異步調用)


@


“異步調用”對應的是“同步調用”,

在實際開發中,有時候為了及時處理請求和進行響應,我們可能使用異步調用,同步調用指程序按照定義順序依次執行,每一行程序都必須等待上一行程序執行完成之后才能執行;異步調用指程序在順序執行時,不等待異步調用的語句返回結果就執行后面的程序。異步調用的實現有很多,例如多線程、定時任務、消息隊列等。

這里學習使用@Async注解來實現異步調用。


1、@EnableAsync

首先,我們需要在啟動類上添加 @EnableAsync 注解來聲明開啟異步方法。

@SpringBootApplication
@EnableAsync
public class SpringbootAsyncApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootAsyncApplication.class, args);
    }

}

2、@Async

需要注意的,@Async在使用上有一些限制:

  • 它只能應用於public修飾的方法
  • 自調用–從同一個類中調用async方法,將不起作用

原因很簡單:

  • 只有公共方法,才可以被代理。
  • 自調用不起作用,因為它越過了代理直接調用了方法。

2.1、無返回值的異步方法

這是一個異步運行的無返回值方法:

    @Async
    public void asyncMethodWithVoidReturnType() {
        System.out.println("異步無返回值方法 "
                + Thread.currentThread().getName());
    }

實例:

  • AsyncTask:異步式任務類,定義了三個異步式方法。
/**
 * @Author 三分惡
 * @Date 2020/7/15
 * @Description 異步式任務
 */
@Component
public class AsyncTask {
   Logger log= LoggerFactory.getLogger(AsyncTask.class);

    private Random random = new Random();

    /**
     * 定義三個異步式方法
     * @throws InterruptedException
     */
    @Async
    public void taskOne() throws InterruptedException {
        long start = System.currentTimeMillis();
        //隨機休眠若干毫秒
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("任務一執行完成耗時{}秒", (end - start)/1000f);
    }

    @Async
    public void taskTwo() throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("任務二執行完成耗時{}秒", (end - start)/1000f);
    }

    @Async
    public void taskThree() throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("任務三執行完成耗時{}秒", (end - start)/1000f);
    }

}
  • 在測試類中調用三個異步式方法:
/**
 * @Author 三分惡
 * @Date 2020/7/15
 * @Description
 */
@SpringBootTest
@RunWith(SpringRunner.class)
public class AsyncTaskTest {

    @Autowired
    private AsyncTask asyncTask;

    Logger log= LoggerFactory.getLogger(AsyncTaskTest.class);

    @Test
    public void doAsyncTasks(){
        try {
            long start = System.currentTimeMillis();
            //調用三個異步式方法
            asyncTask.taskOne();
            asyncTask.taskTwo();
            asyncTask.taskThree();
            Thread.sleep(5000);
            long end = System.currentTimeMillis();
            log.info("主程序執行完成耗時{}秒", (end - start)/1000f);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

運行結果:可以看到三個方法沒有順序執行,這個復執行單元測試,您可能會遇到各種不同的結果,比如:

  • 沒有任何任務相關的輸出
    • 有部分任務相關的輸出
      • 亂序的任務相關的輸出

在這里插入圖片描述

在這里插入圖片描述

原因是目前doTaskOne、doTaskTwo、doTaskThree三個函數的時候已經是異步執行了。主程序在異步調用之后,主程序並不會理會這三個函數是否執行完成了,由於沒有其他需要執行的內容,所以程序就自動結束了,導致了不完整或是沒有輸出任務相關內容的情況。


2.1、有返回值的異步方法

@Async也可以應用有返回值的方法–通過在Future中包裝實際的返回值:

   /**
     * 有返回值的異步方法
     * @return
     */
    @Async
    public Future<String> asyncMethodWithReturnType() {
        System.out.println("執行有返回值的異步方法 "
                + Thread.currentThread().getName());
        try {
            Thread.sleep(5000);
            return new AsyncResult<String>("hello world !!!!");
        } catch (InterruptedException e) {
            //
        }
        return null;
    }

Spring還提供了一個實現Future的AsyncResult類。這個類可用於跟蹤異步方法執行的結果。


實例:

  • 我們將2.1的實例改造成有返回值的異步方法:
    @Async
    public Future<String> taskOne() throws InterruptedException {
        long start = System.currentTimeMillis();
        //隨機休眠若干毫秒
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("任務一執行完成耗時{}秒", (end - start)/1000f);
        return new AsyncResult<>("任務一完事了");
    }

taskTwo、taskThree方法做同樣的改造。

  • 測試有返回值的異步方法:
   @Test
    public void doFutureTask(){
        try {
            long start=System.currentTimeMillis();
            Future<String> future1=asyncTask.taskOne();
            Future <String> future2 = asyncTask.taskTwo();
            Future <String> future3 = asyncTask.taskThree();
            //三個任務執行完再執行主程序
            do {
                Thread.sleep(100);
            } while (future1.isDone() && future2.isDone() && future3.isDone());
            log.info("獲取異步方法的返回值:{}", future1.get());
            Thread.sleep(5000);
            long end = System.currentTimeMillis();
            log.info("主程序執行完成耗時{}秒", (end - start)/1000f);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

運行結果:可以看到三個任務完成后才執行主程序,還輸出了異步方法的返回值。

在這里插入圖片描述


3、 Executor

默認情況下,Spring使用SimpleAsyncTaskExecutor異步運行這些方法。

可以在兩個級別上重寫默認線程池——應用程序級別或方法級別。


3.1、方法級別重寫Executor

所需的執行程序需要在配置類中聲明 Executor:

/**
 * @Author 三分惡
 * @Date 2020/7/15
 * @Description 方法級別重寫線程池
 */
@Configuration
@EnableAsync
public class SpringAsyncConfig {

    @Bean(name = "threadPoolTaskExecutor")
    public Executor threadPoolTaskExecutor() {
        return new ThreadPoolTaskExecutor();
    }
}

然后,在@Async中的屬性提供Executor名稱:

    @Async("threadPoolTaskExecutor")
    public void asyncMethodWithConfiguredExecutor() {
        System.out.println("Execute method with configured executor - "
                + Thread.currentThread().getName());
    }

3.2、應用級別重寫Executor

配置類應實現AsyncConfigurer接口,重寫getAsyncExecutor()方法。

在這里,我們將返回整個應用程序的Executor,這樣一來,它就成為運行以@Async注釋的方法的默認Executor:

/**
 * @Author 三分惡
 * @Date 2020/7/15
 * @Description 應用級別重寫 Excutor
 */
@Configuration
@EnableAsync
public class SpringApplicationAsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        return new ThreadPoolTaskExecutor();
    }
}

3.3、自定義線程池配置

在上面,自定義線程池只是簡單地返回了一個線程池:

return new ThreadPoolTaskExecutor();

實際上,還可以對線程池做一些配置:

/**
 * @Author 三分惡
 * @Date 2020/7/15
 * @Description
 */
@Configuration
@EnableAsync
public class SpringPropertiesAsyncConfig implements AsyncConfigurer {

    /**
     * 對線程池進行配置
     * @return
     */
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(20);
        taskExecutor.setMaxPoolSize(200);
        taskExecutor.setQueueCapacity(25);
        taskExecutor.setKeepAliveSeconds(200);
        taskExecutor.setThreadNamePrefix("oKong-");
        // 線程池對拒絕任務(無線程可用)的處理策略,目前只支持AbortPolicy、CallerRunsPolicy;默認為后者
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        taskExecutor.initialize();
        return taskExecutor;
    }
}

ThreadPoolTaskExecutor配置參數的簡單說明:

  • corePoolSize:線程池維護線程的最少數量

  • keepAliveSeconds:允許的空閑時間,當超過了核心線程出之外的線程在空閑時間到達之后會被銷毀

  • maxPoolSize:線程池維護線程的最大數量,只有在緩沖隊列滿了之后才會申請超過核心線程數的線程

  • queueCapacity:緩存隊列

  • rejectedExecutionHandler:線程池對拒絕任務(無線程可用)的處理策略。這里采用了CallerRunsPolicy策略,當線程池沒有處理能力的時候,該策略會直接在 execute 方法的調用線程中運行被拒絕的任務;如果執行程序已關閉,則會丟棄該任務。還有一個是AbortPolicy策略:處理程序遭到拒絕將拋出運行時RejectedExecutionException。


4、異常處理

當方法返回類型為Future時,異常處理很容易– Future.get()方法將拋出異常。

但是如果是無返回值的異步方法,異常不會傳播到調用線程。因此,我們需要添加額外的配置來處理異常。

我們將通過實現AsyncUncaughtExceptionHandler接口來創建自定義異步異常處理程序。

當存在任何未捕獲的異步異常時,將調用handleUncaughtException()方法:

/**
 * @Author 三分惡
 * @Date 2020/7/15
 * @Description
 */
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
    @Override
    public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
        System.out.println("Exception message - " + throwable.getMessage());
        System.out.println("Method name - " + method.getName());
        for (Object param : objects) {
            System.out.println("Parameter value - " + param);
        }
    }
}

上面,我們使用配置類實現了AsyncConfigurer接口。

作為其中的一部分,我們還需要重寫getAsyncUncaughtExceptionHandler()方法以返回我們的自定義異步異常處理:

    /**
     * 返回自定義異常處理
     * @return
     */
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
         return new CustomAsyncExceptionHandler();
    }

5、總結

這里異步請求的使用及相關配置,如超時,異常等處理。在剝離一些和業務無關的操作時,就可以考慮使用異步調用進行其他無關業務操作,以此提供業務的處理效率。或者一些業務場景下可拆分出多個方法進行同步執行又互不影響時,也可以考慮使用異步調用方式提供執行效率。



本文為學習筆記類博客,學習資料來源見參考!



參考:

【1】:《深入淺出SpringBoot 2.x》
【2】:Spring Boot中使用@Async實現異步調用
【3】:SpringBoot 中異步執行任務的 2 種方式
【4】:How To Do @Async in Spring
【5】:SpringBoot系列:Spring Boot異步調用@Async
【6】:SpringBoot | 第二十一章:異步開發之異步調用
【7】:實戰Spring Boot 2.0系列(三) - 使用@Async進行異步調用詳解


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM