Help us understand the problem. What is going on with this article?

@Asyncを使ってSpring Bootで非同期処理を行う

More than 1 year has passed since last update.

このドキュメントについて

SpringBootで非同期処理を行う際に必要なことを調べたので、この資料では、

  1. @Asyncを使った非同期処理のやり方
  2. 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の設定周りが詳しく書いてありとてもわかりやすかった。なかったらちょっときつかった

参考にした資料

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away