yoshikipom Tech Blog

Spring Bootの@Asyncで非同期処理(thread poolの設定 + 動作確認)

Overview

メインスレッドの処理に大きな影響を与えずにに何かの処理をしたいときに実装した(APIでレスポンスタイムに影響を与えずになにかDBに保存したい等)。 その処理を確実に達成したい場合はMessage Queueをを使った方が無難。

以下のような要件を達成するような実装を紹介する。 - 非同期で実行し、メインスレッドはその完了を待たない - 何らかの理由でThreadが増えすぎてメイン処理を遅くすることは避けたい

Code: https://github.com/yoshikipom/java/tree/main/spring-mvc/src/main/java/com/yoshikipom/dev/api/async Ref - https://www.baeldung.com/spring-async - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.html

Code

非同期処理を実行するThread

@Configuration
public class MyTaskExecutorBean {

  @Bean(name = "myTask")
  public TaskExecutor myTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(1);
    executor.setQueueCapacity(1);
    executor.setMaxPoolSize(2);
    return executor;
  }
}

上記の非同期スレッドで実装したい処理

  • @Async("${bean名}") をメソッドにつける (publicメソッド限定)
@Service
@Slf4j
public class MyAsyncService {

  @Async("myTask")
  public void executeAsyncProcess() {
    log.info("start async process");
    log.info("sleep 1s");
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
    log.info("finish async process");
  }
}

呼び出し側

  • myAsyncService.executeAsyncProcess() が非同期処理
  • @EnableAsync を 実行クラスにつける
  • thread poolの設定のため4回連続で実行
  • maxPoolSize + queueCapacityは受け付けられない。そのときはTaskRejectedException が起こる
@SpringBootApplication
@EnableAsync
@Slf4j
public class MyApplication implements CommandLineRunner {

  @Autowired
  private MyAsyncService myAsyncService;

  public static void main(String[] args) {

    SpringApplication.run(MyApplication.class, args);
  }

  @Override
  public void run(String... args) throws Exception {
    log.info("start app");
    try {
      // max pool size:2, queue size:1
      myAsyncService.executeAsyncProcess(); // will be executed
      myAsyncService.executeAsyncProcess(); // will be executed on another thread
      myAsyncService.executeAsyncProcess(); // will be queued
      myAsyncService.executeAsyncProcess(); // will not be executed due to shortage of thread pool
    } catch (TaskRejectedException e) {
      log.warn(e.getMessage());
    }
    log.info("finish app");
  }
}

実行結果

  • メインの処理が終わってから非同期のメソッドが動いている
  • myTask-1myTask-2の二つは並列で動いている
  • 3つ目の処理はmaxPoolSizeを超えるので上記の2つのどちらかが終わるまで待ってから実行 (今回は myTask-1が先に空いた)
  • 4つ目の処理は受け付けられないため、Errorが発生 (catchに書いたWARN logが出ている)。
2022-07-07 21:44:23.914  INFO 66165 --- [           main] c.y.dev.api.async.MyApplication          : Started MyApplication in 2.479 seconds (JVM running for 2.874)
2022-07-07 21:44:23.916  INFO 66165 --- [           main] c.y.dev.api.async.MyApplication          : start app
2022-07-07 21:44:23.918  WARN 66165 --- [           main] c.y.dev.api.async.MyApplication          : Executor [java.util.concurrent.ThreadPoolExecutor@32eae6f2[Running, pool size = 2, active threads = 2, queued tasks = 1, completed tasks = 0]] did not accept task: org.springframework.aop.interceptor.AsyncExecutionInterceptor$$Lambda$785/0x0000000800fe2a40@550e9be6
2022-07-07 21:44:23.918  INFO 66165 --- [           main] c.y.dev.api.async.MyApplication          : finish app
2022-07-07 21:44:23.924  INFO 66165 --- [       myTask-1] c.y.dev.api.async.MyAsyncService         : start async process
2022-07-07 21:44:23.924  INFO 66165 --- [       myTask-1] c.y.dev.api.async.MyAsyncService         : sleep 1s
2022-07-07 21:44:23.924  INFO 66165 --- [       myTask-2] c.y.dev.api.async.MyAsyncService         : start async process
2022-07-07 21:44:23.924  INFO 66165 --- [       myTask-2] c.y.dev.api.async.MyAsyncService         : sleep 1s
2022-07-07 21:44:24.928  INFO 66165 --- [       myTask-1] c.y.dev.api.async.MyAsyncService         : finish async process
2022-07-07 21:44:24.928  INFO 66165 --- [       myTask-2] c.y.dev.api.async.MyAsyncService         : finish async process
2022-07-07 21:44:24.929  INFO 66165 --- [       myTask-1] c.y.dev.api.async.MyAsyncService         : start async process
2022-07-07 21:44:24.929  INFO 66165 --- [       myTask-1] c.y.dev.api.async.MyAsyncService         : sleep 1s
2022-07-07 21:44:25.933  INFO 66165 --- [       myTask-1] c.y.dev.api.async.MyAsyncService         : finish async process

Conclusion

Thread数(とキューイングする数)を制限した非同期処理が実装できた。 CompletableFutureFuture を非同期処理の戻り値に使えば非同期処理を待って戻り値を得ることも可能。