このドキュメントについて
SpringBootで非同期処理を行う際に必要なことを調べたので、この資料では、
- @Asyncを使った非同期処理のやり方
- TaskExcecuterを使ったスレッドの設定
についてまとめた。
実行環境
java8 + Spring Boot 2.1.0 + lombok
build.gradleは以下の通り
buildscript {
ext {
springBootVersion = '2.1.0.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
implementation('org.springframework.boot:spring-boot-starter-web')
compileOnly('org.projectlombok:lombok')
testImplementation('org.springframework.boot:spring-boot-starter-test')
}
やってみる
非同期で実行したい処理を実装する
実装
適当にスリープするだけの処理をつくる
@Slf4j
@Service
public class AsyncService {
@Async("Thread2")
public CompletableFuture<String> findName(String name, Long sleepTime) throws InterruptedException {
log.warn("Async function started. processName: " + name + "sleepTime: " + sleepTime);
Thread.sleep(sleepTime);
// 非同期処理のハンドリングができるようにCompletableFutureに実際に使いたい返却値をぶっこんで利用する
return CompletableFuture.completedFuture(name);
}
}
解説
- 処理自体はログはいてスリープしているだけ
- 返却値は非同期処理がハンドリングできるようにCompletableFutureのオブジェクトを返却する
- 実際に使いたい値をCompletableFutureに食わせて返却すると関数の呼び元でも利用できる
- 非同期の処理にしたい関数には@Asyncをつける
- @Asyncの中に記載されている"Thread2"については後述する
上記のロジックを呼んでハンドリングする処理を実装する
実装
適当なControllerを実装して非同期処理をハンドリングする
@Slf4j
@RequiredArgsConstructor
@RestController
public class FindNameContoller {
private final AsyncService asyncService;
@GetMapping("/users/")
public List<String> findUsers() throws Exception {
long start = System.currentTimeMillis();
long heavyProcessTime = 3000L;
long lightProcessTime = 1000L;
log.warn("request started");
CompletableFuture<String> heavyProcess = asyncService.findName("heavy", heavyProcessTime);
CompletableFuture<String> lightProcess = asyncService.findName("light", lightProcessTime);
// heavyProcessが終わったら実行される処理
heavyProcess.thenAcceptAsync(heavyProcessResult -> {
log.warn("finished name=" + heavyProcessResult + ", sleep = " + heavyProcessTime);
});
// lightProcessが終わったら実行される処理
lightProcess.thenAcceptAsync(lightProcessResult -> {
log.warn("finished name=" + lightProcessResult + ", sleep = " + lightProcessTime);
});
// 指定した処理が終わったらこれ以降の処理が実行される
CompletableFuture.allOf(heavyProcess, lightProcess).join();
// 返却値の作成
List<String> names = new ArrayList<>();
names.add(heavyProcess.get());
names.add(lightProcess.get());
Thread.sleep(10L);
long end = System.currentTimeMillis();
// 処理全体の時間を出力
log.warn("request finished. time: " + ((end - start)) + "ms");
return names;
}
}
解説
- 全体の処理としては重い処理と軽い処理を非同期で並列に実行して、全部の処理が実行されたらログはいて終了するという簡単なもの
- CompletableFutureの関数か、返却値のCompletableFuture型のオブジェクトの関数を利用して非同期処理のハンドリングをしてる
- 個別に行う場合は返却値のオブジェクトを利用するが、全部終わるの待つ、という感じであればCompletableFuture.allOfを利用する
- いくつかハンドリングのやり方があるみたいだがここでは割愛する
Threadの上限等の設定をする
実装
メインのApplicationのクラスにもろもろ設定を記載していく
@EnableAsync
@SpringBootApplication
public class AsyncTrainingApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncTrainingApplication.class, args);
}
@Bean("Thread1") // この設定は指定していないので利用されていない
public Executor taskExecutor1() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setThreadNamePrefix("Thread1--");
executor.initialize();
return executor;
}
@Bean("Thread2") // ここで設定した"Thread2"を@Asyncに設定するとその設定が利用される
public Executor taskExecutor2() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // デフォルトのThreadのサイズ。あふれるとQueueCapacityのサイズまでキューイングする
executor.setQueueCapacity(1); // 待ちのキューのサイズ。あふれるとMaxPoolSizeまでThreadを増やす
executor.setMaxPoolSize(500); // どこまでThreadを増やすかの設定。この値からあふれるとその処理はリジェクトされてExceptionが発生する
executor.setThreadNamePrefix("Thread2--");
executor.initialize();
return executor;
}
@Bean("Reject") // この設定にするとキューイングできないしThreadも増やせないしでRejectedExecutionExceptionが発生する
public Executor rejectTaskExecuter() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(1);
executor.setQueueCapacity(0);
executor.setMaxPoolSize(1);
executor.setThreadNamePrefix("Reject--");
executor.initialize();
return executor;
}
}
解説
- @EnableAsyncをつけると@Asyncが使えるようになる
- @Beanに記載した文字列で設定がマッチングされる。今回の場合は"Thread2"のBeanが利用される
- TaskExecuterの設定をしていない場合はデフォルトで動いているっぽい(つっこんで調べてない)
- コメントにも記載したが、下記のような流れでThreadがつくられる
- CorePoolSizeまでThreadをつくる
- ThreadがいっぱいになるとQueueCapacityまでキューイングされる
- キューイングからあふれるとMaxPoolSizeまでThreadがつくられる
- MaxPoolSizeからあふれるとRejectedExecutionExceptionが発生する
実行する
処理を実行させると下記のようなログがでて非同期で動いていることがわかる
2018-11-27 20:19:13.181 WARN 8711 --- [nio-8080-exec-5] c.e.asynctraining.FindNameContoller : request started
2018-11-27 20:19:13.181 WARN 8711 --- [TaskExecutor-31] com.example.asynctraining.AsyncService : Async function started. processName: heavysleepTime: 3000
2018-11-27 20:19:13.182 WARN 8711 --- [TaskExecutor-32] com.example.asynctraining.AsyncService : Async function started. processName: lightsleepTime: 1000
2018-11-27 20:19:14.187 WARN 8711 --- [onPool-worker-2] c.e.asynctraining.FindNameContoller : finished name=light, sleep = 1000
2018-11-27 20:19:16.182 WARN 8711 --- [onPool-worker-2] c.e.asynctraining.FindNameContoller : finished name=heavy, sleep = 3000
2018-11-27 20:19:16.194 WARN 8711 --- [nio-8080-exec-5] c.e.asynctraining.FindNameContoller : request finished. time: 3013ms
ざっくりまとめ
- 非同期で動かしたい処理はCompletableFutureで返却して@Asyncをつける
- 非同期処理のハンドリングはCompletableFutureをうまいこと使う
- @EnableAsyncを@Configurationついてるクラスにつける
- Threadの設定はTaskExecuterで行い、内部の設定がわかりづらいので気をつけること
感想
- 公式のリファレンスを見ながらやったがかなり簡単に導入できると感じた
- Threadのチューニングが面倒臭そうだと思った。ベタな設定とかがあるなら知りたいがパフォーマンステストしながらやらないとダメそうだと感じた
- 慎さんのブログにThreadの設定周りが詳しく書いてありとてもわかりやすかった。なかったらちょっときつかった