SpringBoot 非同步程式設計淺談

空慧居士發表於2023-12-18

1. 需求背景

  當我們需要提高系統的併發效能時,我們可以將耗時的操作非同步執行,從而避免執行緒阻塞,提高系統的併發效能。例如,在處理大量的併發請求時,如果每個請求都是同步阻塞的方式處

理,系統的響應時間會變得很長。而使用非同步程式設計,可以將一些耗時的操作交給其他執行緒去處理,從而釋放主執行緒,提高系統的併發能力。

2. SpringBoot如何實現非同步呼叫

  從Spring 3開始,可以透過在方法上標註@Async註解來實現非同步方法呼叫。這意味著當我們呼叫被@Async註解修飾的方法時,它會在後臺以非同步方式執行。為了啟用非同步功能,我們需要

一個配置類,並在該類上使用@EnableAsync註解。這個註解告訴Spring要開啟非同步功能。

3. 非同步呼叫實現步驟

第一步:新建配置類,開啟@Async功能支援

  使用@EnableAsync來開啟非同步任務支援,@EnableAsync註解可以直接放在SpringBoot啟動類上,也可以單獨放在其他配置類上。這裡選擇使用單獨的配置類SyncConfiguration

使用@Async註解,在預設情況下用的是SimpleAsyncTaskExecutor執行緒池,該執行緒池不是真正意義上的執行緒池

使用此執行緒池無法實現執行緒重用,每次呼叫都會新建一條執行緒。若系統中不斷的建立執行緒,最終會導致系統佔用記憶體過高,引發OutOfMemoryError錯誤,所以在使用Spring中的@Async非同步

框架時要自定義執行緒池,替代預設的SimpleAsyncTaskExecutor,這也是自定義配置的意義之一。

@Configuration
@EnableAsync
public class SyncConfiguration {
    @Bean(name = "asyncPoolTaskExecutor")
    public ThreadPoolTaskExecutor executor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        //核心執行緒數,設定核心執行緒數。核心執行緒數是執行緒池中一直保持活動的執行緒數量,即使它們是空閒的。
        taskExecutor.setCorePoolSize(10);
        //設定執行緒池維護執行緒的最大數量。當緩衝佇列已滿並且核心執行緒數的執行緒都在忙碌時,執行緒池會建立新的執行緒,直到達到最大執行緒數。
        taskExecutor.setMaxPoolSize(100);
        //設定緩衝佇列的容量。當所有的核心執行緒都在忙碌時,新的任務將會被放入緩衝佇列中等待執行。
        taskExecutor.setQueueCapacity(50);
        //設定非核心執行緒的空閒時間。當超過核心執行緒數的執行緒在空閒時間達到設定值後,它們將被銷燬,以減少資源的消耗。
        taskExecutor.setKeepAliveSeconds(200);
        //非同步方法內部執行緒名稱
        taskExecutor.setThreadNamePrefix("async-");
        /**
         * 當執行緒池的任務快取佇列已滿並且執行緒池中的執行緒數目達到maximumPoolSize,如果還有任務到來就會採取任務拒絕策略
         * 通常有以下四種策略:
         * ThreadPoolExecutor.AbortPolicy:丟棄任務並丟擲RejectedExecutionException異常。
         * ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不丟擲異常。
         * ThreadPoolExecutor.DiscardOldestPolicy:丟棄佇列最前面的任務,然後重新嘗試執行任務(重複此過程)
         * ThreadPoolExecutor.CallerRunsPolicy:重試新增當前的任務,自動重複呼叫 execute() 方法,直到成功
         */
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        taskExecutor.initialize();
        return taskExecutor;
    }
}

注:

Spring提供了多種執行緒池:

  • SimpleAsyncTaskExecutor:不是真的執行緒池,這個類不重用執行緒,每次呼叫都會建立一個新的執行緒。

  • SyncTaskExecutor:這個類沒有實現非同步呼叫,只是一個同步操作。只適用於不需要多執行緒的地

  • ConcurrentTaskExecutor:Executor的適配類,不推薦使用。如果ThreadPoolTaskExecutor不滿足要求時,才用考慮使用這個類

  • ThreadPoolTaskScheduler:可以使用cron表示式

  • ThreadPoolTaskExecutor :最常使用,推薦。 其實質是對java.util.concurrent.ThreadPoolExecutor的包裝

第二步:在方法上標記非同步呼叫

在非同步處理的方法上新增@Async註解,代表該方法為非同步處理。

public class AsyncTask {

    @Async
    public void Task() {
        long t1 = System.currentTimeMillis();
        Thread.sleep(5000);
        long t2 = System.currentTimeMillis();
        log.info("task cost {} ms" , t2-t1);
    }

注:

在非同步程式設計中,如果需要處理帶有返回值的非同步方法(有則繼續瀏覽,無則跳過),Spring提供了java.util.concurrent.Future介面和java.util.concurrent.CompletableFuture類來處理非同步任務的返回值。

1. 使用Future介面:Future介面表示一個非同步計算的結果。我們可以透過呼叫Future物件的get()方法來獲取非同步任務的返回值,但這將阻塞當前執行緒,直到非同步任務完成並返回結果

@Service
public class MyService {
    @Async
    public Future<String> asyncMethodWithReturnValue() {
        // 模擬耗時操作
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        return new AsyncResult<>("Async method result");
    }
}

上述示例中,asyncMethodWithReturnValue()方法使用@Async註解標記為非同步方法,並返回一個Future<String>物件。在方法內部,我們使用AsyncResult類來建立一個包含非同步結果的Future物件。

在呼叫該非同步方法時,可以使用get()方法來獲取非同步任務的返回值。但需要注意,get()方法會阻塞當前執行緒,直到非同步任務執行完成並返回結果。

@Autowired
private MyService myService;

public void someMethod() {
    Future<String> futureResult = myService.asyncMethodWithReturnValue();
    
    // 阻塞等待非同步任務完成並獲取返回值
    try {
        String result = futureResult.get();
        System.out.println("Async method result: " + result);
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}

上述示例中,我們透過呼叫futureResult.get()方法來獲取非同步任務的返回值。如果非同步任務還未完成,呼叫get()方法將會阻塞當前執行緒,直到非同步任務完成並返回結果。

2. 使用CompletableFuture類:CompletableFuture是Java 8引入的一個強大的非同步程式設計工具,提供了更加靈活和功能豐富的非同步任務處理方式。

@Service
public class MyService {
    @Async
    public CompletableFuture<String> asyncMethodWithReturnValue() {
        // 模擬耗時操作
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        return CompletableFuture.completedFuture("Async method result");
    }
}

在上述示例中,asyncMethodWithReturnValue()方法使用@Async註解標記為非同步方法,並返回一個CompletableFuture<String>物件。在方法內部,我們使用CompletableFuture.completedFuture()方法建立一個包含非同步結果的CompletableFuture物件。

在呼叫該非同步方法時,可以鏈式呼叫thenApply()thenAccept()等方法來對非同步任務的結果進行處理。

@Autowired
private MyService myService;

public void someMethod() {
    CompletableFuture<String> futureResult = myService.asyncMethodWithReturnValue();
    
    futureResult.thenAccept(result -> {
        System.out.println("Async method result: " + result);
    });
    
    // 執行其他操作
    
    // 阻塞等待非同步任務完成
    futureResult.join();
}

在上述示例中,我們透過呼叫thenAccept()方法來處理非同步任務的結果,而不需要顯式呼叫get()方法。thenAccept()方法接受一個Consumer函式式介面,用於處理非同步任務的結果。

此外,CompletableFuture還提供了豐富的方法,例如thenApplyAsync()thenCompose()thenCombine()等,用於處理複雜的非同步任務流程。

注:

當使用CompletableFuture處理非同步任務時,以下是thenApplyAsync()thenCompose()thenCombine()thenAccept()這四個方法的區別

  • thenApplyAsync()thenAccept()用於處理非同步任務的結果,並返回一個新的CompletableFuture或不返回任何結果。
  • thenCompose()用於處理非同步任務的結果,並返回一個新的CompletableFuture,該結果是另一個CompletionStage的結果。
  • thenCombine()用於組合兩個非同步任務的結果,並應用指定的函式處理結果,並返回一個新的CompletableFuture

第三步:在需要進行非同步執行的地方進行呼叫

asyncTask.Task();

  

4. @Async的原理

  1. 當一個帶有@Async註解的方法被呼叫時,Spring會建立一個非同步代理物件來代理這個方法的呼叫。

  2. 非同步代理物件會將方法呼叫封裝為一個獨立的任務,並將該任務提交給非同步任務執行器。

  3. 非同步任務執行器從執行緒池中獲取一個空閒的執行緒,並將任務分配給該執行緒執行。

  4. 呼叫執行緒立即返回,不會等待非同步任務的執行完成。

  5. 非同步任務在獨立的執行緒中執行,直到任務完成。

  6. 非同步任務執行完成後,可以選擇返回結果或者不返回任何結果。

 

相關文章