Spring Boot 中的異步調用


通常我們開發的程序都是同步調用的,即程序按照代碼的順序一行一行的逐步往下執行,每一行代碼都必須等待上一行代碼執行完畢才能開始執行。而異步編程則沒有這個限制,代碼的調用不再是阻塞的。所以在一些情景下,通過異步編程可以提高效率,提升接口的吞吐量。這節將介紹如何在Spring Boot中進行異步編程。

 

開啟異步

新建一個Spring Boot項目,版本為2.1.0.RELEASE,並引入spring-boot-starter-web依賴,項目結構如下所示:

要開啟異步支持,首先得在Spring Boot入口類上加上@EnableAsync注解:

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

 

接下來開始編寫異步方法。

com.example.demo路徑下新建service包,並創建TestService

@Service
public class TestService {

  private Logger logger = LoggerFactory.getLogger(this.getClass());

  @Async
  public void asyncMethod() {
      sleep();
      logger.info("異步方法內部線程名稱:{}", Thread.currentThread().getName());
  }

  public void syncMethod() {
      sleep();
  }

  private void sleep() {
      try {
          TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
  }
}

 

上面的Service中包含一個異步方法asyncMethod(開啟異步支持后,只需要在方法上加上@Async注解便是異步方法了)和同步方法syncMethodsleep方法用於讓當前線程阻塞2秒鍾。

接着在com.example.demo路徑下新建controller包,然后創建TestController

@RestController
public class TestController {

  private Logger logger = LoggerFactory.getLogger(this.getClass());

  @Autowired
  private TestService testService;

  @GetMapping("async")
  public void testAsync() {
      long start = System.currentTimeMillis();
      logger.info("異步方法開始");

      testService.asyncMethod();

      logger.info("異步方法結束");
      long end = System.currentTimeMillis();
      logger.info("總耗時:{} ms", end - start);
  }

  @GetMapping("sync")
  public void testSync() {
      long start = System.currentTimeMillis();
      logger.info("同步方法開始");

      testService.syncMethod();

      logger.info("同步方法結束");
      long end = System.currentTimeMillis();
      logger.info("總耗時:{} ms", end - start);
  }
}

 

啟動項目,訪問 http://localhost:8080/sync 請求,控制台輸出如下:

可看到默認程序是同步的,由於sleep方法阻塞的原因,testSync方法執行了2秒鍾以上。

訪問 http://localhost:8080/async ,控制台輸出如下:

可看到testAsync方法耗時極少,因為異步的原因,程序並沒有被sleep方法阻塞,這就是異步調用的好處。同時異步方法內部會新啟一個線程來執行,這里線程名稱為task - 1。

默認情況下的異步線程池配置使得線程不能被重用,每次調用異步方法都會新建一個線程,我們可以自己定義異步線程池來優化。

自定義異步線程池

com.example.demo下新建config包,然后創建AsyncPoolConfig配置類:

@Configuration
public class AsyncPoolConfig {

  @Bean
  public ThreadPoolTaskExecutor asyncThreadPoolTaskExecutor(){
      ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
      executor.setCorePoolSize(20);
      executor.setMaxPoolSize(200);
      executor.setQueueCapacity(25);
      executor.setKeepAliveSeconds(200);
      executor.setThreadNamePrefix("asyncThread");
      executor.setWaitForTasksToCompleteOnShutdown(true);
      executor.setAwaitTerminationSeconds(60);

      executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

      executor.initialize();
      return executor;
  }
}

上面我們通過ThreadPoolTaskExecutor的一些方法自定義了一個線程池,這些方法的含義如下所示:

  • corePoolSize:線程池核心線程的數量,默認值為1(這就是默認情況下的異步線程池配置使得線程不能被重用的原因)。

  • maxPoolSize:線程池維護的線程的最大數量,只有當核心線程都被用完並且緩沖隊列滿后,才會開始申超過請核心線程數的線程,默認值為Integer.MAX_VALUE

  • queueCapacity:緩沖隊列。

  • keepAliveSeconds:超出核心線程數外的線程在空閑時候的最大存活時間,默認為60秒。

  • threadNamePrefix:線程名前綴。

  • waitForTasksToCompleteOnShutdown:是否等待所有線程執行完畢才關閉線程池,默認值為false。

  • awaitTerminationSecondswaitForTasksToCompleteOnShutdown的等待的時長,默認值為0,即不等待。

  • rejectedExecutionHandler:當沒有線程可以被使用時的處理策略(拒絕任務),默認策略為abortPolicy,包含下面四種策略:

    1. callerRunsPolicy:用於被拒絕任務的處理程序,它直接在 execute 方法的調用線程中運行被拒絕的任務;如果執行程序已關閉,則會丟棄該任務。

    2. abortPolicy:直接拋出java.util.concurrent.RejectedExecutionException異常。

    3. discardOldestPolicy:當線程池中的數量等於最大線程數時、拋棄線程池中最后一個要執行的任務,並執行新傳入的任務。

    4. discardPolicy:當線程池中的數量等於最大線程數時,不做任何動作。

要使用該線程池,只需要在@Async注解上指定線程池Bean名稱即可:

@Service
public class TestService {
  ......

  @Async("asyncThreadPoolTaskExecutor")
  public void asyncMethod() {
      ......
  }
  ......
}

重啟項目,再次訪問 http://localhost:8080/async ,控制台輸出入下:

處理異步回調

如果異步方法具有返回值的話,需要使用Future來接收回調值。我們修改TestServiceasyncMethod方法,給其添加返回值:

@Async("asyncThreadPoolTaskExecutor")
public Future<String> asyncMethod() {
  sleep();
  logger.info("異步方法內部線程名稱:{}", Thread.currentThread().getName());
  return new AsyncResult<>("hello async");
}

 

泛型指定返回值的類型,AsyncResult為Spring實現的Future實現類:

接着改造TestControllertestAsync方法:

@GetMapping("async")
public String testAsync() throws Exception {
  long start = System.currentTimeMillis();
  logger.info("異步方法開始");

  Future<String> stringFuture = testService.asyncMethod();
  String result = stringFuture.get();
  logger.info("異步方法返回值:{}", result);
   
  logger.info("異步方法結束");

  long end = System.currentTimeMillis();
  logger.info("總耗時:{} ms", end - start);
  return stringFuture.get();
}

 

Future接口的get方法用於獲取異步調用的返回值。

重啟項目,訪問 http://localhost:8080/async 控制台輸出如下所示:

通過返回結果我們可以看出Futureget方法為阻塞方法,只有當異步方法返回內容了,程序才會繼續往下執行。get還有一個get(long timeout, TimeUnit unit)重載方法,我們可以通過這個重載方法設置超時時間,即異步方法在設定時間內沒有返回值的話,直接拋出java.util.concurrent.TimeoutException異常。

比如設置超時時間為60秒:

String result = stringFuture.get(60, TimeUnit.SECONDS);


免責聲明!

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



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