LoginSignup
2
2
記事投稿キャンペーン 「2024年!初アウトプットをしよう」

Java21のVirtualThreadsに対する自分の理解

Last updated at Posted at 2024-01-28

VirtualThreadsとは

JVM内で生成される軽量なスレッド。

そもそも、JavaはスレッドをOSで生成する。OSで生成されたスレッド(以下:OSスレッド)はJVMで生成されるスレッドより重い。だが、その分色んな処理を実行できる。

例えば、IO処理。(DBアクセス、ファイルアクセス、外部APIコール等)

WebアプリにIO処理はつきもの。
IO処理の厄介なところはIO待ちが発生すること。
OSスレッドはIO待ちが発生するとそれを待ち続けてしまう。
待ちが増えるということは、スループットが落ちるということ。
(スループット=単位時間あたりに実行された処理の数)

これをなんとかするために導入されたのが、Virtual Threads.(仮想スレッド)

vt.jpg

Virtual Threadsを理解する上で知っておくべき登場人物は↓の3つ

  • Platform, Carrier, Virtual
    • Platform Thread
      • OSスレッドを薄くラップしたスレッド
      • ≒OSスレッドだと思って良さそう?
    • Carrier Thread
      • Virtualからマウント/アンマウントされるスレッド
      • 実体はPlatform Threadらしい
    • Virtual Thread
      • JVM内で生成されるスレッド
      • JVM内で大量に生成して、Carrierにマウント

クライアントからリクエストが来たら、まずJVMでVTを生成する。
VTはCTにマウントされ、IO処理を始める。
IO待ちが発生したら、VTはCTからアンマウントされ別の処理を始める。
IO待ちが解消したらVTに知らせて(Interrupt?)、CTに再びマウントして中断していた処理を再開する。

結果、IO待ちの無駄な時間に別の処理を実行できるため、スループットは向上する。

仮想スレッドは高速なスレッドではありません。プラットフォーム スレッドよりも高速にコードを実行することはありません。これらは、速度 (待ち時間の短縮) ではなく、スケール (スループットの向上) を提供するために存在します。

https://docs.oracle.com/en/java/javase/20/core/virtual-threads.html#GUID-BEC799E0-00E9-4386-B220-8839EA6B4F5C

SpringBootで活用するには

下記をapplication.propertiesに設定するだけ。簡単。

spring.threads.virtual.enabled=true

【検証】 platform vs virtual

パフォーマンスを検証してみる。
検証に使ったソースコードはこちら。

スレッド数の違いを検証

以下のサンプルコードを使って、PlatformとVirtualの違いを確認する。

Contorller
package io.github.tttol.virtualthreadexample.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import io.github.tttol.virtualthreadexample.service.VirtualThreadsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;


@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping
public class VirtualThreadsController {
    private final VirtualThreadsService virtualThreadsService;

    @GetMapping("/platform/{count}")
    public String doPlatform(@PathVariable int count) {
        log.info("start platform. count={}", count);
        virtualThreadsService.execPlatformThread(count);
        return "platform";
    }

    @GetMapping("/virtual/{count}")
    public String doVirtual(@PathVariable int count) {
        log.info("start virtual. count={}", count);
        virtualThreadsService.execVirtualThread(count);
        return "virtual";
    }
}
Service
package io.github.tttol.virtualthreadexample.service;

import java.util.stream.IntStream;

import org.springframework.stereotype.Service;


@Service
public class VirtualThreadsService {
    public void sleep() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            System.out.println("Interrupted!");//適当
        }
    }

    public void execPlatformThread(int count) {
        IntStream.range(0, count)
        .forEach(e -> Thread.ofPlatform().start(() -> sleep()));
    }

    public void execVirtualThread(int count) {
        IntStream.range(0, count)
        .forEach(e -> Thread.ofVirtual().start(() -> sleep()));
    }
}

スレッド数の推移はjconsoleを使ってモニタリングする。

まずはPlatform。
curlした瞬間に200スレッド一気にどばっと増えている↓

curl http://localhost:8080/platform/200

スクリーンショット 2024-01-27 12.54.00.png

次にVirtual。
10スレッドほど増えただけ↓

curl http://localhost:8080/virtual/200

スクリーンショット 2024-01-27 12.52.11.png

JMeterで負荷を加えたときの検証

JMeterで1000リクエストほどの負荷をかけてみて、スループットとレイテンシを計測してみる。

JMeterの使い方に関しては以下記事が非常にわかりやすかった。

Platform/Virtualの切り替えはspring.threads.virtual.enabledのtrue/falseで切り替えて比較する。

Controllerに以下のようなメソッドを作って、これをリクエストすることで計測する。

Controller
    @GetMapping("sleep")
    public String sleep() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            log.error("Interrupted!", e);
        }
        return "sleep";
    }
スループット[/sec] 最小レイテンシ[sec] 最大レイテンシ[sec] 平均レイテンシ[sec]
Platform Thread 14.6 5.001 47.520 26.216
Virtual Thread 124.8 5.000 5.133 5.006

スループットが約10倍、レイテンシもVirtualのほうはほぼ5秒で完結している。
Platformのほうは遅延がひどい。

次に、sleep時間をもう少し長く10秒にしてみる。

Controller
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
スループット[/sec] 最小レイテンシ[sec] 最大レイテンシ[sec] 平均レイテンシ[sec]
Platform Thread 10.0 10.001 97.543 53.737
Virtual Thread 76.9 10.000 10.059 10.003

Virtualのほうは10秒台で完結。Platformは平均53秒近くかかっていしまっている。

ただし、5秒のときも10秒のときも、Virtual Threadの処理自体が高速になっているわけではない。
スループットが向上するだけである点に注意。

参考

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2