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
- Beanとして登録する
- Defaultでも
task
という名前のものが登録されている - 個人的には新規で作りたい
- 命名により目的が明示的
- 調査時にログにスレッド名が出たときにわかりやすい
- 設定が明示的
- 複数のプールを定義可能
- ここでした設定
- corePoolSize: キープするスレッドプール数
- queueCapacity: スレッドが足りないときに待ち状態にできる数
- maxPoolSize: 最大のスレッド数
- 他の設定はドキュメントを参照 https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.html
@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-1
とmyTask-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数(とキューイングする数)を制限した非同期処理が実装できた。
CompletableFuture
や Future
を非同期処理の戻り値に使えば非同期処理を待って戻り値を得ることも可能。