天天看點

深入了解Spring的@Async注解:實作異步方法調用

作者:程式猿阿嘴

簡介

在當今高速發展的應用開發領域,對于提升系統性能和響應能力的需求越來越迫切。而異步程式設計作為一種解決方案,已經成為現代應用開發中的一項重要技術。本篇部落格将帶您深入探究 Java 中的 @Async 注解,揭示其強大的異步執行能力和精妙的實作機制。

異步程式設計是一種程式設計模式,通過将任務分解為多個子任務,并在背景或并行線程中執行這些子任務,以提高程式的性能和響應能力。

@Async 注解簡介

@Async 注解是 Spring 架構提供的注解,用于将方法标記為異步執行的方法。它的作用是告訴 Spring 架構在調用被注解的方法時,将其放入線程池中異步執行,而不是阻塞等待方法的完成。

@Async 注解的工作原理是,在調用被注解的方法時,Spring 會将該方法的執行轉移到線程池中的一個線程進行處理。執行完成後,方法的傳回值将通過 Future 或 CompletableFuture 進行封裝,以便擷取方法的傳回結果。

  1. @Async 注解适用于以下場景,并具有以下優勢:
  2. 網絡請求:在處理網絡請求時,可以使用 @Async 注解将請求發送和響應處理分離,提高系統的并發處理能力。
  3. 耗時計算:對于需要耗費大量時間的計算任務,可以使用 @Async 注解将計算過程放在背景執行,避免阻塞主線程,提高系統的響應速度。
  4. 并行處理:通過 @Async 注解,可以同時執行多個任務,将多個互相獨立的任務并行處理,進而減少整體處理時間。
  5. 響應能力提升:使用異步程式設計可以避免阻塞主線程,提高系統的并發能力和響應能力,增強使用者體驗。
  6. 代碼簡化:使用 @Async 注解可以簡化程式設計模型,将異步執行的邏輯與業務邏輯分離,使代碼更清晰、易于維護。
  7. 異步執行通過将任務分解為多個并發執行的子任務,可以充分利用系統資源,提高系統的吞吐量和并發處理能力,進而提升系統的性能和響應能力。@Async 注解簡化了異步程式設計的實作,使開發人員能夠更友善地使用異步處理機制。同時,它還可以使代碼更易于閱讀和維護,提高開發效率。

@Async 注解的源碼解析

@Async 注解在 Spring 架構中的實作主要依賴于以下幾個關鍵元件:

  • AsyncAnnotationBeanPostProcessor:這是一個 Bean 後置處理器,負責解析帶有 @Async 注解的方法,将其包裝成異步任務。
  • AsyncTaskExecutor:這是一個任務執行器,用于執行異步任務。可以通過配置來指定具體的線程池或任務排程器。
  • AsyncConfigurer:這是一個可選的接口,用于提供自定義的異步任務執行器。

在 Spring 架構中,當啟用異步支援時,AsyncAnnotationBeanPostProcessor 會掃描容器中的 Bean,并檢查其中的方法是否标記有 @Async 注解。如果發現帶有 @Async 注解的方法,它将會将其封裝成一個代理對象,并注冊為一個可執行的異步任務。

當調用被 @Async 注解标記的方法時,實際上是調用了該方法的代理對象。代理對象會将方法的執行轉移到線程池中的一個線程進行處理,并傳回一個 Future 對象,用于擷取方法的傳回結果。

線程池的配置可以通過 Spring 的配置檔案或程式設計方式進行指定。可以配置線程池的大小、線程池的類型(如固定大小線程池、緩存線程池等)以及任務排程政策等。

異步方法與事務的關系

在使用 @Async 注解标記的異步方法與事務之間存在一些關系和注意事項。

  1. 預設情況下,異步方法不受事務管理的影響。當一個帶有 @Transactional 注解的方法調用一個标記為 @Async 的異步方法時,異步方法将在一個新的線程中執行,與原始方法的事務無關。
  2. 異步方法獨立事務。如果希望異步方法能夠參與到事務管理中,可以使用 Propagation.REQUIRES_NEW 傳播行為。将異步方法設定為 @Transactional(propagation = Propagation.REQUIRES_NEW) ,這樣異步方法将在新的事務中執行,與原始方法的事務隔離開來。
  3. 異步方法和事務的送出。由于異步方法是在獨立的線程中執行的,與原始方法的事務是分離的。是以,異步方法中的事務送出操作不會對原始方法的事務産生影響。即使異步方法中的事務送出失敗,也不會導緻原始方法的事務復原。
  4. 異步方法和事務的異常處理。異步方法中的異常預設是不會被捕獲和處理的,除非在異步方法中顯式地進行了異常處理。如果需要對異步方法中的異常進行處理,可以使用 AsyncUncaughtExceptionHandler 接口來自定義異常處理邏輯。

需要注意的是,使用異步方法與事務的組合可能會帶來一些潛在的問題和風險,如資料不一緻性、并發沖突等。在使用異步方法和事務的同時,需要仔細考慮業務需求和資料一緻性的要求,確定邏輯正确性和資料完整性。

總結起來,異步方法和事務之間的關系可以通過設定事務的傳播行為來調整。預設情況下,異步方法是獨立于事務的,可以通過設定 Propagation.REQUIRES_NEW 傳播行為使異步方法參與到事務管理中。然而,需要注意并發和資料一緻性的問題,并根據具體業務需求合理使用異步方法和事務的組合。

Async異常處理

在使用 @Async進行異步方法調用時,異常處理是一個重要的方面。以下是異步方法的異常處理機制:

  1. 預設情況下,異步方法的異常會被捕獲并封裝為Future對象(或CompletableFuture對象)。您可以通過Future.get() 方法或CompletableFuture.get() 方法擷取異步任務的結果,并在調用時捕獲異常。如果異步任務抛出異常,将會在調用get() 方法時重新抛出異常,您可以在調用端進行異常處理。
typescript複制代碼@Async
public CompletableFuture<String> performTask() {
    // 異步任務邏輯
}

// 調用異步方法并處理異常
CompletableFuture<String> future = myService.performTask();
try {
    String result = future.get();
    // 處理正常結果
} catch (InterruptedException | ExecutionException e) {
    // 處理異常情況
}
           
  1. 您還可以使用AsyncUncaughtExceptionHandler接口來處理異步方法中未捕獲的異常。通過實作AsyncUncaughtExceptionHandler接口,并在AsyncConfigurer中重寫getAsyncUncaughtExceptionHandler() 方法,您可以定義全局的異步異常處理邏輯。
less複制代碼@Configuration
    @EnableAsync
    public class AsyncConfig implements AsyncConfigurer {

        // 配置異步方法執行器
        @Override
        public Executor getAsyncExecutor() {
            // 配置任務執行器
        }

        // 配置異步方法未捕獲異常處理器
        @Override
        public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
            return new CustomAsyncExceptionHandler();
        }

        // 其他配置...
    }
           
typescript複制代碼public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        // 處理異步方法未捕獲的異常
        Class<?> clazz = method.getDeclaringClass();
        String message = String.format("異步方法執行失敗,具體類名: %s, 方法名:%s, 異常資訊: %s", clazz.getName(), method.getName(), ex);
        log.error("異步方法執行失敗,具體類名: {}, 方法名:{}, 方法入參:{}, 異常資訊: {}", clazz.getName(), method.getName(), Arrays.toString(params), ex.getMessage(), ex);
    }
}
           

在上述示例中,CustomAsyncExceptionHandler實作了AsyncUncaughtExceptionHandler接口,并實作了handleUncaughtException() 方法來處理異步方法中未捕獲的異常。您可以在該方法中編寫自定義的異常處理邏輯,例如日志記錄、錯誤報警等。

通過上述異常處理機制,您可以捕獲和處理異步方法中的異常,進而確定對異步任務的異常情況進行适當的處理。

ThreadLocal和Async使用問題

在工作過程中,經常遇到這個問題,系統通常會通過攔截器擷取使用者資訊并設定到ThreadLoacl中,但是在異步方法中擷取使用者資訊,卻出現了擷取到了其他使用者資訊的問題。 這是因為@Async注解會在異步執行方法時切換線程,而線程切換會導緻ThreadLocal中的内容無法被正确傳遞。

解決這個問題的一種方法是使用AsyncTaskExecutor的子類,例如ThreadPoolTaskExecutor,并在配置中設定TaskDecorator。TaskDecorator可以在每次異步任務執行時對線程進行修飾,以確定ThreadLocal中的内容被正确傳遞。

以下是一個示例配置的代碼片段:

typescript複制代碼@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setTaskDecorator(new ThreadLocalTaskDecorator()); // 設定TaskDecorator
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }

    // 自定義的TaskDecorator
    private static class ThreadLocalTaskDecorator implements TaskDecorator {
        @Override
        public Runnable decorate(Runnable runnable) {
            // 儲存目前線程的ThreadLocal内容
             // 擷取調用線程的 traceId 和使用者資訊
            String traceId = ThreadLocalUtils.getTraceId();
            User user = ThreadLocalUtils.getUser();
            return () -> {
                try {
                    // 恢複之前儲存的ThreadLocal内容
                     // 在子線程中設定 traceId 和使用者資訊
                    ThreadLocalUtils.setTraceId(traceId);
                    ThreadLocalUtils.setUser(user);
                    runnable.run();
                } finally {
                   // 清除子線程的 traceId 和使用者資訊
                   ThreadLocalUtils.clear();
                }
            };
        }
    }
}
           

在上述示例中,我們使用ThreadLocalContextHolder類來管理ThreadLocal的操作,包括設定、擷取和清理ThreadLocal中的内容。

csharp複制代碼public class ThreadLocalUtils {
    private static final ThreadLocal<String> traceIdThreadLocal = new ThreadLocal<>();
    private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>();

    public static String getTraceId() {
        return traceIdThreadLocal.get();
    }

    public static void setTraceId(String traceId) {
        traceIdThreadLocal.set(traceId);
    }

    public static User getUser() {
        return userThreadLocal.get();
    }

    public static void setUser(User user) {
        userThreadLocal.set(user);
    }

    public static void clear() {
        traceIdThreadLocal.remove();
        userThreadLocal.remove();
    }
}
           

通過使用以上的配置和ThreadLocalTaskDecorator,你可以確定在異步執行時,ThreadLocal中的使用者資訊能夠正确傳遞并被擷取到。

多線程池配置

如果您需要配置多個不同類型的 @Async注解,并且使用不同的線程池類型(緩存線程池和固定線程池),可以按照以下方式進行配置: 首先,建立多個線程池和相應的TaskExecutor bean。

scss複制代碼@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    // 緩存線程池
    @Bean("cachedThreadPool")
    public TaskExecutor cachedThreadPool() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(0); // 根據實際情況調整核心線程數
        executor.setMaxPoolSize(Integer.MAX_VALUE); // 根據實際情況調整最大線程數
        executor.setQueueCapacity(100); // 根據實際情況調整隊列容量
        executor.setThreadNamePrefix("cached-thread-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setTaskDecorator(new MdcTaskDecorator()); // 設定任務裝飾器
        executor.initialize();
        return executor;
    }

    // 固定線程池
    @Bean("fixedThreadPool")
    public TaskExecutor fixedThreadPool() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10); // 根據實際情況調整核心線程數
        executor.setMaxPoolSize(10); // 根據實際情況調整最大線程數
        executor.setQueueCapacity(0); // 不使用隊列,直接執行
        executor.setThreadNamePrefix("fixed-thread-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setTaskDecorator(new MdcTaskDecorator()); // 設定任務裝飾器
        executor.initialize();
        return executor;
    }

    // 配置異步方法執行器
    @Override
    public Executor getAsyncExecutor() {
        return cachedThreadPool();
    }

    // 配置自定義的異步方法執行器,用于特定類型的異步任務
    @Bean("customAsyncExecutor")
    public Executor customAsyncExecutor() {
        return fixedThreadPool();
    }

    // 其他配置...

}
           

在上述示例中,我們建立了兩個不同類型的線程池:cachedThreadPool和fixedThreadPool,并将它們作為TaskExecutor bean 注冊到Spring容器中。

通過以上配置,您可以使用不同的線程池類型為不同類型的異步任務配置不同的執行器,并根據需求調整線程池的屬性。

最佳實踐和注意事項

在使用異步方法時,需要注意以下幾點:

  • 異步方法應盡量保持簡單和獨立,不涉及複雜的事務邏輯。
  • 異步方法的執行時間應控制在合理的範圍内,避免因長時間執行導緻線程資源占用過多。
  • 需要考慮異步方法與其他業務邏輯的協調,確定異步方法的執行順序和結果正确性。
  • 異步方法的并發性可能導緻資源競争和并發通路的問題,需要進行适當的并發控制和線程安全處理。

作者:阿勁

連結:https://juejin.cn/post/7239715390805033019