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

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

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

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

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

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

![vt.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159675/b203fe86-a960-21ff-b905-193fe9e31592.jpeg)

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待ちの無駄な時間に別の処理を実行できるため、スループットは向上する。

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

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


# SpringBootで活用するには
下記をapplication.propertiesに設定するだけ。簡単。
```properties
spring.threads.virtual.enabled=true
```


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

https://github.com/tttol/virtual-thread-example


### スレッド数の違いを検証
以下のサンプルコードを使って、PlatformとVirtualの違いを確認する。

```java: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";
    }
}
```

```java: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スレッド一気にどばっと増えている↓
```shell
curl http://localhost:8080/platform/200
```

![スクリーンショット 2024-01-27 12.54.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159675/167acf57-bf62-7734-5e72-16363433f846.png)


次にVirtual。
10スレッドほど増えただけ↓
```shell
curl http://localhost:8080/virtual/200
```
![スクリーンショット 2024-01-27 12.52.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159675/27e78b51-60d2-b356-ea27-71174e0c2876.png)

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

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

https://tech-blog.rakus.co.jp/entry/2017/08/24/111332


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

Controllerに以下のようなメソッドを作って、これをリクエストすることで計測する。
```java: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秒にしてみる。

```java: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の処理自体が高速になっているわけではない。
スループットが向上するだけである点に注意。


# 参考
https://qiita.com/nowokay/items/174f75b9e48cc7a80838#444-virtual-threads

https://speakerdeck.com/skrb/virtual-thread-dao-ru-nobei-jing-to-xiao-guo-de-nashi-ifang-372a94a6-b5dc-448d-b1d1-e8750efdafcb

https://docs.google.com/presentation/d/17RdIx4ysrebxOTrGrBl2pr7yn9-T2krhyi9833C8pBE/edit#slide=id.p

https://www.baeldung.com/spring-6-virtual-threads