提升Spring Boot應用效能的秘密武器:揭秘@Async註解的實用技巧

码农Academy發表於2024-03-11

引言

在日常業務開發中,非同步程式設計已成為應對併發挑戰和提升應用程式效能的關鍵策略。傳統的同步程式設計方式,由於會阻礙主執行緒執行後續任務直至程式程式碼執行結束,不可避免地降低了程式整體效率與響應速度。因此,為克服這一瓶頸,開發者廣泛採用非同步程式設計技術,將那些可能阻塞的長時間執行任務委派至後臺執行緒處理,從而確保主執行緒始終保持高效和靈敏的響應能力。

SpringBoot作為一款廣受歡迎的應用開發框架,極大地簡化了非同步程式設計實踐。其中,@Async註解是SpringBoot為實現非同步程式設計提供的便捷工具之一。透過巧妙地應用@Async註解,開發者能夠無縫地將方法呼叫轉化為非同步執行模式,進而增強系統的併發效能表現。

本文將深度剖析SpringBoot中的@Async註解,包括其內在原理、具體使用方法以及相關注意事項。我們將深入探討@Async的工作機制,展示如何在實際的SpringBoot專案中有效運用該註解。

@Async的原理

SpringBoot中,@Async註解的實現原理基於Spring框架的AOP和任務執行器(Task Executor)機制。

@Async的啟用

開啟對非同步方法的支援需要在配置類上新增@EnableAsync註解,然後就可以啟用了一個Bean後處理器:AsyncConfigurationSelector,它負責自動配置AsyncConfigurer,為非同步方法提供所需的執行緒池。
image.png

AsyncConfigurationSelector中預設使用PROXY的代理,即使用ProxyAsyncConfiguration,而ProxyAsyncConfiguration是用於配置Spring非同步方法的代理模式的配置類。

image.png

當然我們還可以指定使用另外一個代理模式:AdviceMode.ASPECTJ,以便使用AspectJ來進行更高階的攔截和處理。

它繼承至AbstractAsyncConfiguration,在AbstractAsyncConfiguration中配置AsyncConfigurersetConfigurers方法用於設定非同步任務執行器和異常處理器。

image.png

AsyncConfigurer中提供了一種便捷的方式來配置非同步方法的執行器(AsyncTaskExecutor)。透過實現AsyncConfigurer介面,可以自定義非同步方法的執行策略、執行緒池等配置資訊。預設情況下Spring會先搜尋TaskExecutor型別的bean或者名字為taskExecutorExecutor型別的bean,都不存在使用SimpleAsyncTaskExecutor執行器。

public interface AsyncConfigurer {  
	/*
	* 該方法用於獲取一個AsyncTaskExecutor物件,用於執行非同步方法。
	* 可以在這個方法中建立並配置自定義的AsyncTaskExecutor,例如ThreadPoolTaskExecutor或SimpleAsyncTaskExecutor等。
	*/
    @Nullable  
    default Executor getAsyncExecutor() {  
       return null;  
    }  

	/*
	* 該方法用於獲取一個AsyncUncaughtExceptionHandler物件,用於處理非同步方法執行中未捕獲的異常。如果非同步方法執行過程中出現異常而沒有被捕獲,Spring會呼叫這個方法來處理異常。
	* 可以在這個方法中返回自定義的AsyncUncaughtExceptionHandler實現,以實現對非同步方法異常的處理邏輯。
	*/
    @Nullable  
    default AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {  
       return null;  
    }  
  
}

同時ProxyAsyncConfiguration中的AsyncAnnotationBeanPostProcessor會掃描應用上下文中的所有Bean,檢查它們的方法是否標記了@Async註解。對於標記了@Async註解的方法,AsyncAnnotationBeanPostProcessor會建立一個代理物件,用於在呼叫該方法時啟動一個新的執行緒或使用執行緒池執行該方法。這樣就實現了非同步執行的功能。同時它還負責處理@Async註解中的其他屬性,例如設定非同步方法的執行超時時間、指定執行緒池名稱等。

非同步方法註解與代理

當服務類的方法被@Async註解修飾時,Spring AOP會檢測到這個註解,並利用動態代理技術為該類建立一個代理物件。其他元件透過Spring容器呼叫帶有@Async註解的方法時,實際上是呼叫了代理物件的方法。

一個帶有@Async註解的方法被呼叫時,Spring AOP會攔截這個方法呼叫。此時就會觸發處理非同步呼叫的核心攔截器:AsyncExecutionInterceptor。它的主要任務是將被@Async修飾的方法封裝成一個Runnable或者Callable任務,並將其提交給TaskExecutor管理的執行緒池去執行。這個過程確保了非同步方法的執行不會阻塞呼叫者執行緒。

image.png

TaskExecutor與執行緒池

TaskExecutor是一個介面,定義瞭如何執行RunnableCallable任務。SpringBoot提供了多種實現,如SimpleAsyncTaskExecutorThreadPoolTaskExecutor等。通常我們會自定義一個ThreadPoolTaskExecutor以滿足特定需求,比如設定核心執行緒數、最大執行緒數、佇列大小等引數,以確保非同步任務能夠高效併發執行。AsyncExecutionInterceptor將非同步方法封裝的任務提交給配置好的TaskExecutor管理的執行緒池執行。

非同步方法執行與結果返回

非同步方法的實際執行在獨立的執行緒中進行,不阻塞呼叫者執行緒。非同步方法的返回型別可以是voi 或者具有返回值,如果非同步方法有返回值,那麼返回型別通常應該是java.util.concurrent.Future,這樣呼叫者可以透過Future物件來檢查非同步任務是否完成以及獲取最終的結果。

@Async使用

SpringBoot中,使用@Async註解可以輕鬆地將方法標記為非同步執行。下面來看一下如何在Spring Boot專案中正確地使用@Async註解,包括配置方法和注意事項。

在方法上新增@Async註解

要使用@Async註解,首先需要在要非同步執行的方法上新增該註解。這樣Spring就會在呼叫這個方法時將其封裝為一個非同步任務,並交給執行緒池執行。

@Service  
public class AsyncTaskService {  
  
    /**  
     * 透過@Async 註解表明該方法是個非同步方法,  
     * @param i  
     */  
    @Async  
    public void executeAsyncTask(Integer i) {  
        System.out.println(Thread.currentThread().getName()+" 執行非同步任務:" + i);  
    }
}    

啟用非同步功能

SpringBoot應用中,需要在配置類上新增@EnableAsync註解來啟用對非同步方法的支援。

@EnableAsync  
@SpringBootApplication  
public class SpringBootBaseApplication {  
  
    public static void main(String[] args) {  
       SpringApplication.run(SpringBootBaseApplication.class, args);  
    }  
}

配置執行緒池

預設情況下,SpringBoot會使用一個預設的執行緒池來執行非同步任務(SimpleAsyncTaskExecutor)。但是,為了更好地控制執行緒池的行為,我們可以自定義ThreadPoolTaskExecutor,並透過AsyncConfigurer進行配置。

@Configuration  
public class TaskExecutorConfig implements AsyncConfigurer{  
  
  
    @Override  
    public Executor getAsyncExecutor() {  
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();  
        executor.setCorePoolSize(5);  
        executor.setQueueCapacity(25);  
        executor.setMaxPoolSize(10);  
        executor.setThreadNamePrefix("MyAsyncThread-");  
        executor.initialize();  
        return executor;  
    }
 }   

測試

@SpringBootTest  
public class SpringBootBaseApplicationTests {  
  
    @Autowired  
    private AsyncTaskService asyncTaskService;  
  
    @Test  
    public void asyncTest() {  
       for (int i = 0; i < 10; i++) {  
          asyncTaskService.executeAsyncTask(i);  
       }  
    }  
  
}

輸出結果如下:

image.png

注意事項

  1. @Async必須配合@EnableAsync註解一起使用。兩者缺一不可。

  2. 非同步方法必須定義在Spring Bean中,因為Spring AOP是基於代理物件來實現的。假如我們把AsyncTaskService類中的@Service去掉。就不建立Bean。然後測試程式碼中修改為如下:

@Test  
public void asyncTest() {  
    AsyncTaskService asyncTaskService = new AsyncTaskService();  
    for (int i = 0; i < 10; i++) {  
       asyncTaskService.executeAsyncTask(i);  
    }  
}

執行結果如下:
image.png

都是主執行緒同步方法。

  1. 非同步方法不能定義為private或static,因為Spring AOP無法攔截這些方法。我們修改AsyncTaskService類中的方法修改為private或者static,則會發生編譯錯誤:

image.png
image.png

  1. 非同步方法內部的呼叫不能使用this關鍵字,因為this關鍵字是代理物件的引用,會導致非同步呼叫失效。
@Service  
public class AsyncTaskService {
	@Async  
	public void asyncMethod() {  
	    System.out.println("Async method executed in thread: " + Thread.currentThread().getName());  
	}  
	  
	  
	public void callAsyncMethod() {  
	    // 在同一個類中直接呼叫非同步方法  
	    this.asyncMethod(); // 這裡呼叫不會觸發非同步執行  
	    System.out.println("callAsyncMethod executed in thread: " + Thread.currentThread().getName());  
	}
}	
  1. @Async註解修飾的方法不能直接被同一個類中的其他方法呼叫。原因是Spring會在執行時生成一個代理類,呼叫非同步方法時實際上是呼叫這個代理類的方法。因此,如果在同一個類中直接呼叫非同步方法,@Async註解將不會生效,因為這樣呼叫會繞過代理物件,導致非同步執行失效。
@Service  
public class AsyncTaskService {

	@Async  
	public void asyncMethod() {  
	    System.out.println("Async method executed in thread: " + Thread.currentThread().getName());  
	}  
	  
	public void callAsyncMethod() {  
	    // 在同一個類中直接呼叫非同步方法  
	    asyncMethod(); // 這裡呼叫不會觸發非同步執行  
	    System.out.println("callAsyncMethod executed in thread: " + Thread.currentThread().getName());  
	}
}

測試程式碼:

@SpringBootTest  
public class SpringBootBaseApplicationTests {  
  
    @Autowired  
    private AsyncTaskService asyncTaskService;  
  
    @Test  
    public void asyncTest2(){  
       asyncTaskService.callAsyncMethod();  
    }  
  
}

執行結果如下:

image.png

  1. 不同的非同步方法間不要相互呼叫
    非同步方法間的相互呼叫會顯著增加程式碼的複雜性層級,由於非同步執行的本質在於即時返回並延遲完成任務,因此,巢狀或遞迴式的非同步呼叫容易導致邏輯難以梳理和維護,特別是在涉及多非同步操作狀態追蹤、順序控制及依賴關係管理時尤為突出。

當非同步方法內部進一步呼叫其他非同步方法,並且牽涉到同步資源如鎖、訊號量等時,極易引發死鎖問題。例如,一個執行緒在等待自身啟動的另一個非同步任務結果的同時,該任務卻嘗試獲取第一個執行緒所持有的鎖,如此迴圈等待,形成無法解開的死鎖。

無節制地在非同步方法內部啟動新的非同步任務,特別是在高併發場景下,可能導致執行緒池資源迅速耗盡,使得系統喪失處理更多請求的能力。此外,直接的非同步方法呼叫還增加了錯誤處理與日誌記錄的難度,特別是遇到異常情況時,往往難以追溯原始呼叫鏈路以精準定位問題源頭。

若需要確保非同步方法按照特定順序執行,直接呼叫會導致邏輯混亂不清。為解決這一問題,通常推薦採用回撥機制、Future/CompletionStage鏈式程式設計、響應式程式設計模型(如RxJava、Project Reactor)等方式來確保有序執行並降低耦合度。

同時,頻繁且低延遲的任務間直接互相呼叫可能會引入額外的上下文切換開銷,從而對系統的整體效能造成潛在負面影響。

  1. 合理配置執行緒池
    Spring Boot預設提供的執行緒池配置可能無法充分滿足特定應用在複雜多變生產環境下的需求,例如其預設的執行緒數、佇列大小和拒絕策略等引數可能不盡合理。為確保資源的有效管理和精細控制,我們可以透過自定義執行緒池來靈活設定核心執行緒數、最大執行緒數、執行緒空閒超時時間、任務等待佇列容量以及飽和策略(如任務拒絕策略)等關鍵屬性,從而適應不同業務場景對併發執行任務數量及資源消耗的精準調控。

另外,不同型別非同步任務具有不同的執行特性:有的任務耗時較長,而有的則短促且頻繁。針對這種情況,為各類任務配置獨立的執行緒池有助於實現更好的資源隔離,避免任務間的相互影響,進而保障系統的穩定性和響應速度。同時,為了滿足特定的安全規範或效能要求,自定義執行緒池還可以支援諸如設定守護執行緒、優先順序、執行緒命名格式化等功能。

更重要的是,自定義執行緒池有利於系統內部執行狀態的深度監控與問題診斷。透過制定合理的命名規則、詳盡的日誌記錄以及精確的metrics統計分析,我們可以清晰洞察每個執行緒池的工作狀況,及時發現並最佳化潛在的效能瓶頸。

如果不進行自定義執行緒池配置,僅依賴於預設或簡化的執行緒池實現,在面對大量湧入的任務時,可能會因執行緒資源耗盡導致整個系統響應能力和可用性受損。因此,採用合理配置的自定義執行緒池能夠在高負載環境下有效防範此類風險,有力支撐系統的穩健執行。

@Configuration  
public class TaskExecutorConfig implements AsyncConfigurer{  
  
  
    @Override  
    public Executor getAsyncExecutor() {  
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();  
        // 核心執行緒數  
        executor.setCorePoolSize(5);  
        // 設定佇列容量  
        executor.setQueueCapacity(25);  
        // 最大執行緒數  
        executor.setMaxPoolSize(10);  
        // 自定義執行緒名稱字首  
        executor.setThreadNamePrefix("MyAsyncThread-");  
        executor.initialize();  
        return executor;  
    }
}    

關於自定義執行緒池的引數講解請參考我這篇文章:重溫Java基礎(二)之Java執行緒池最全詳解

  1. 異常處理:
    非同步方法內部的異常通常不會被呼叫方捕獲到,因此需要在非同步方法內部進行異常處理,可以透過try-catch塊:
@Async  
public void asyncMethod() {  
    try {  
        // 非同步操作程式碼  
    } catch (Exception ex) {  
        log.error("Error occurred in async method", ex);  
        // 其他錯誤處理邏輯  
    }  
}

或者使用@Async註解的exceptionHandler屬性來處理異常並進行適當的日誌記錄或錯誤處理。

@Configuration  
public class TaskExecutorConfig implements AsyncConfigurer{

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

// 自定義異常處理器
class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {  
  
    @Override  
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {  
        log.error("Uncaught exception in async method: " + method.getName(), ex);  
        // 其他錯誤處理邏輯  
    }  
}
  1. 與事務的互動
    預設情況下,當我們在Spring應用中使用@Async註解標記一個方法為非同步執行時,這個方法將不會參與到其呼叫者所處的事務上下文中。這意味著,如果呼叫非同步方法的方法在一個事務內執行,該事務將在呼叫非同步方法後正常提交或回滾,但非同步方法內部的操作並不會受到這個事務的影響。

例如,若在同步方法中修改了資料庫記錄,並隨後呼叫了一個非同步方法來更新其他相關的資料,那麼如果同步方法中的事務在呼叫非同步方法後提交,而非同步方法在執行過程中丟擲了異常導致更新失敗,這時第一部分已提交的資料和第二部分未成功更新的資料之間就會產生不一致的情況。

為了確保非同步方法能夠正確地參與事務管理,可以透過設定@Async註解的事務傳播行為屬性(@Transactionalpropagation屬性值)來解決這個問題。

@Transactional(propagation = Propagation.REQUIRES_NEW)  
@Async  
public void asyncMethod() {  
    System.out.println("Async method executed in thread: " + Thread.currentThread().getName());  
    // 具體業務
}

這裡透過設定Propagation.REQUIRES_NEW,指示Spring在執行非同步方法時開啟一個新的、與當前事務無關的事務。這樣即使非同步方法內部發生異常,它自己的事務會獨立進行提交或回滾,從而保證了資料的一致性。不過要注意的是,這種做法可能會增加系統資源消耗,因為每次非同步任務都會建立新的事務上下文。

總結

透過本文的介紹,我們瞭解了SpringBoot@Async註解的原理、使用方法以及需要注意的事項。

@Async註解能夠將方法標記為非同步執行,利用了Spring框架的AOP和任務執行器機制,使得非同步方法能夠在後臺執行緒池中併發執行,提高系統的併發能力和響應性。

然而,在使用@Async註解時,需要注意避免非同步方法之間相互呼叫,合理配置執行緒池,進行異常處理,處理上下文丟失以及與事務的正確互動。這些注意事項能夠確保非同步方法的可靠性和穩定性,提高應用程式的效能和可維護性。

總的來說,@Async註解是SpringBoot中用於實現非同步方法的重要特性,能夠有效地提升應用程式的效能和併發能力,但在使用時需要謹慎考慮其使用場景和注意事項,以充分發揮其優勢。

本文已收錄於我的個人部落格:碼農Academy的部落格,專注分享Java技術乾貨,包括Java基礎、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中介軟體、架構設計、面試題、程式設計師攻略等

相關文章