CLOVER🍀

That was when it all began.

Virtual Threadsを䜿っおHTTPサヌバヌクラむアントを曞いお、スレッドたわりの動きを確認しおみるスレッドダンプの取埗付き

これはなにをしたくお曞いたもの

前回の゚ントリヌで、JEP 444Virtual Threadsに぀いお曞きたした。

Java 21で正式版になったJEP 444(Virtual Threads)に関するAPIを試す - CLOVER🍀

この時には螏み蟌たなかった、スレッドたわりの挙動やスレッドダンプなどを確認しおみたいず思いたす。

JEP 444Virtual Threadsに぀いお

Virtual Threadsに぀いおは、あらためおは説明したせん。

こちらの゚ントリヌを参照、ずいうこずで。

Java 21で正式版になったJEP 444(Virtual Threads)に関するAPIを試す - CLOVER🍀

今回は、Virtual Threadsの䞭でも以䞋の点に着目しお芋おいこうず思いたす。

  • 仮想スレッドはプラットフォヌムスレッドがマりントしお駆動し、状況によっおアンマりントされる
  • マりントアンマりント
    • IOなどのブロック操䜜でアンマりントされる
    • ReentrantLockでアンマりントされる
    • synchronizedブロックメ゜ッドではアンマりントできない
    • CPUバりンドな凊理ずは盞性が悪い
  • 新しい圢匏のスレッドダンプ

なお、仮想スレッドをアンマりントできない状態のこずをpinningピン留めずいいたす。pinningピン留めが発生した時に
スタックトレヌスを出力する方法もあるのですが、それはこちらに曞きたした。

JEP 444(Virtual Threads)のpinning(ピン留め)をシステムプロパティjdk.tracePinnedThreadsによるスタックトレースの出力で確認する - CLOVER🍀

お題

JDKのHttpServerを䜿っお、Virtual Threadsを䜿った簡単なHTTPサヌバヌを曞いおみたす。

HttpServer (Java SE 21 & JDK 21)

䜜成したHTTPサヌバヌに察しお、いろいろ確認しおいっおみようず思いたす。たた、最埌にHTTPクラむアントを䜿っおテストも
しおみたす。

環境

今回の環境は、こちら。

$ java --version
openjdk 21.0.1 2023-10-17
OpenJDK Runtime Environment (build 21.0.1+12-Ubuntu-222.04)
OpenJDK 64-Bit Server VM (build 21.0.1+12-Ubuntu-222.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 21.0.1, vendor: Private Build, runtime: /usr/lib/jvm/java-21-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-91-generic", arch: "amd64", family: "unix"

CPUは8぀ありたす。

$ cat /proc/cpuinfo | grep processor | wc -l
8

準備

Maven䟝存関係などはこちら。

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.24.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.1.2</version>
                <configuration>
                    <forkCount>2</forkCount>
                </configuration>
            </plugin>
        </plugins>
    </build>

JDKのHttpServerを䜿うだけなら特に䟝存関係は芁らないのですが、テスト甚にJUnitずAssertJを入れおいたす。

Maven Surefire PluginにforkCountを蚭定しおいるのは、テストクラスごずにシステムプロパティをリセットしたいからです。
※テスト内で䜿甚しおいたすが、それは埌述

HTTPサヌバヌを曞く

それでは、お題ずなるHTTPサヌバヌを曞いおみたす。

src/main/java/org/littlewings/virtualthreads/SimpleHttpServer.java

package org.littlewings.virtualthreads;

import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;

public class SimpleHttpServer {
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss");

    private HttpServer httpServer;

    SimpleHttpServer(HttpServer httpServer) {
        this.httpServer = httpServer;
    }

    public static void main(String... args) {
        String host;
        int port;

        if (args.length > 1) {
            host = args[0];
            port = Integer.parseInt(args[1]);
        } else if (args.length > 0) {
            host = "localhost";
            port = Integer.parseInt(args[0]);
        } else {
            host = "localhost";
            port = 8080;
        }

        SimpleHttpServer simpleHttpServer = SimpleHttpServer.create(host, port);
        simpleHttpServer.start();

        Runtime.getRuntime().addShutdownHook(Thread.ofPlatform().unstarted(simpleHttpServer::stop));
    }

    private static void log(String message) {
        Thread currentThread = Thread.currentThread();
        String threadName = currentThread.getName();
        System.out.printf("[%s] - %s - %s%n", LocalDateTime.now().format(FORMATTER), threadName, message);
    }

    public static SimpleHttpServer create(String host, int port) {
        try {
            HttpServer httpServer = HttpServer.create(new InetSocketAddress(host, port), 0);

            log(String.format("jdk.virtualThreadScheduler.parallelism = %s", System.getProperty("jdk.virtualThreadScheduler.parallelism", "")));
            log(String.format("jdk.virtualThreadScheduler.maxPoolSize = %s", System.getProperty("jdk.virtualThreadScheduler.maxPoolSize", "")));

            // httpServer.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
            httpServer.setExecutor(Executors.newThreadPerTaskExecutor(Thread.ofVirtual().name("handler-", 1).factory()));
            httpServer.createContext("/", createHandler());

            return new SimpleHttpServer(httpServer);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public static SimpleHttpServer create(int port) {
        return create("localhost", port);
    }

    static HttpHandler createHandler() {
        ReentrantLock lock = new ReentrantLock();
        ReentrantLock lock2 = new ReentrantLock();
        Object synchronizedLockObject = new Object();
        Object synchronizedLockObject2 = new Object();

        return httpExchange -> {
            URI requestUri = httpExchange.getRequestURI();
            String method = httpExchange.getRequestMethod();
            String requestPath = requestUri.getPath();

            log(String.format("access[%s:%s] start", method, requestPath));

            Consumer<String> writeResponse = responseString -> {
                byte[] binary = responseString.getBytes(StandardCharsets.UTF_8);

                try {
                    httpExchange.sendResponseHeaders(200, binary.length);

                    try (OutputStream os = httpExchange.getResponseBody()) {
                        os.write(binary);
                    }
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            };

            Duration sleepTime = Duration.ofSeconds(3L);

            switch (requestPath) {
                case "/sleep" -> {
                    try {
                        TimeUnit.SECONDS.sleep(sleepTime.toSeconds());
                    } catch (InterruptedException e) {
                        // ignore
                    }
                    writeResponse.accept("sleep.");
                }
                case "/heavy" -> {
                    long startTime = System.currentTimeMillis();

                    while (true) {
                        for (int i = 0; i < 100000; i++) {
                            // loop
                        }

                        long elapsedTime = System.currentTimeMillis() - startTime;

                        if (elapsedTime > sleepTime.toMillis()) {
                            break;
                        }
                    }

                    writeResponse.accept("heavy.");
                }
                case "/lock" -> {
                    try {
                        lock.lock();

                        TimeUnit.SECONDS.sleep(sleepTime.toSeconds());
                    } catch (InterruptedException e) {
                        // ignore
                    } finally {
                        lock.unlock();
                    }
                    writeResponse.accept("lock.");
                }
                case "/lock2" -> {
                    try {
                        lock2.lock();

                        TimeUnit.SECONDS.sleep(sleepTime.toSeconds());
                    } catch (InterruptedException e) {
                        // ignore
                    } finally {
                        lock2.unlock();
                    }
                    writeResponse.accept("lock2.");
                }
                case "/synchronized-lock" -> {
                    synchronized (synchronizedLockObject) {
                        try {
                            TimeUnit.SECONDS.sleep(sleepTime.toSeconds());
                        } catch (InterruptedException e) {
                            // ignore
                        }
                        writeResponse.accept("synchronized lock.");
                    }
                }
                case "/synchronized-lock2" -> {
                    synchronized (synchronizedLockObject2) {
                        try {
                            TimeUnit.SECONDS.sleep(sleepTime.toSeconds());
                        } catch (InterruptedException e) {
                            // ignore
                        }
                        writeResponse.accept("synchronized lock2.");
                    }
                }
                default -> writeResponse.accept("Hello World.");
            }

            log(String.format("access[%s:%s] end", method, requestPath));
        };
    }

    public void start() {
        httpServer.start();
        log(String.format("simple http server[%s:%d], started.", httpServer.getAddress().getHostString(), httpServer.getAddress().getPort()));
    }

    public void stop() {
        httpServer.stop(1);
        log(String.format("simple http server[%s:%d], shutdown.", httpServer.getAddress().getHostString(), httpServer.getAddress().getPort()));
    }
}

なんかたあたあのボリュヌムになりたした 。

HttpServerのむンスタンスを構築しおいる郚分。

    public static SimpleHttpServer create(String host, int port) {
        try {
            HttpServer httpServer = HttpServer.create(new InetSocketAddress(host, port), 0);

            log(String.format("jdk.virtualThreadScheduler.parallelism = %s", System.getProperty("jdk.virtualThreadScheduler.parallelism", "")));
            log(String.format("jdk.virtualThreadScheduler.maxPoolSize = %s", System.getProperty("jdk.virtualThreadScheduler.maxPoolSize", "")));

            // httpServer.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
            httpServer.setExecutor(Executors.newThreadPerTaskExecutor(Thread.ofVirtual().name("handler-", 1).factory()));
            httpServer.createContext("/", createHandler());

            return new SimpleHttpServer(httpServer);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

システムプロパティjdk.virtualThreadScheduler.parallelismずjdk.virtualThreadScheduler.maxPoolSizeは今回ちょっずポむントに
なるので、ログ出力するようにしおいたす。

HTTPサヌバヌはVirtual Threadsで動かすようにしおいたす。

            // httpServer.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
            httpServer.setExecutor(Executors.newThreadPerTaskExecutor(Thread.ofVirtual().name("handler-", 1).factory()));

単玔にExecutors#newVirtualThreadPerTaskExecutorでもよかったのですが、こちらで䜜成するずスレッド名が空になるようなので
ThreadFactoryを䜜る前にnameを指定するようにしおいたす。こうするず、第2匕数で䞎えた倀がむンクリメントされおいきたす。

ログ出力甚のメ゜ッドはこちらで、アクセス時にスレッド名がわかるようにしおいたす。

    private static void log(String message) {
        Thread currentThread = Thread.currentThread();
        String threadName = currentThread.getName();
        System.out.printf("[%s] - %s - %s%n", LocalDateTime.now().format(FORMATTER), threadName, message);
    }

リク゚ストを受け付けた埌の凊理を行うメ゜ッドはこちら。

    static HttpHandler createHandler() {
        ReentrantLock lock = new ReentrantLock();
        ReentrantLock lock2 = new ReentrantLock();
        Object synchronizedLockObject = new Object();
        Object synchronizedLockObject2 = new Object();

        return httpExchange -> {
            URI requestUri = httpExchange.getRequestURI();
            String method = httpExchange.getRequestMethod();
            String requestPath = requestUri.getPath();

            log(String.format("access[%s:%s] start", method, requestPath));

            〜省略〜

            Duration sleepTime = Duration.ofSeconds(3L);

            switch (requestPath) {
                // アクセスパスごずの凊理
            }

            log(String.format("access[%s:%s] end", method, requestPath));
        };
    }

アクセスパスに応じお、以䞋の5皮類の凊理を行いたす。

  • /sleep 
 指定した秒数だけTimeUnit#sleepでスリヌプブロック操䜜の代わり
  • /heavy 
 指定した秒数だけルヌプCPUを消費する凊理
  • /lock、/lock2 
 それぞれ異なるReentrantLockのむンスタンスを䜿っおロックを取埗し、指定した秒数だけTimeUnit#sleepでスリヌプ
  • /synchronized-lock、/synchronized-lock2 
 それぞれ異なるむンスタンスに察しおsynchronizedでロックを取埗し、指定した秒数だけTimeUnit#sleepでスリヌプ
  • それ以倖のパス 
 即座にHello World.を返す

アクセスパスごずの凊理は、caseになっおいるのでそれぞれ曞いおいきたす。

/sleep。

                case "/sleep" -> {
                    try {
                        TimeUnit.SECONDS.sleep(sleepTime.toSeconds());
                    } catch (InterruptedException e) {
                        // ignore
                    }
                    writeResponse.accept("sleep.");
                }

/heavy。

                case "/heavy" -> {
                    long startTime = System.currentTimeMillis();

                    while (true) {
                        for (int i = 0; i < 100000; i++) {
                            // loop
                        }

                        long elapsedTime = System.currentTimeMillis() - startTime;

                        if (elapsedTime > sleepTime.toMillis()) {
                            break;
                        }
                    }

                    writeResponse.accept("heavy.");
                }

/lock、/lock2。

                case "/lock" -> {
                    try {
                        lock.lock();

                        TimeUnit.SECONDS.sleep(sleepTime.toSeconds());
                    } catch (InterruptedException e) {
                        // ignore
                    } finally {
                        lock.unlock();
                    }
                    writeResponse.accept("lock.");
                }
                case "/lock2" -> {
                    try {
                        lock2.lock();

                        TimeUnit.SECONDS.sleep(sleepTime.toSeconds());
                    } catch (InterruptedException e) {
                        // ignore
                    } finally {
                        lock2.unlock();
                    }
                    writeResponse.accept("lock2.");
                }

/synchronized-lock、/synchronized-lock2。

                case "/synchronized-lock" -> {
                    synchronized (synchronizedLockObject) {
                        try {
                            TimeUnit.SECONDS.sleep(sleepTime.toSeconds());
                        } catch (InterruptedException e) {
                            // ignore
                        }
                        writeResponse.accept("synchronized lock.");
                    }
                }
                case "/synchronized-lock2" -> {
                    synchronized (synchronizedLockObject2) {
                        try {
                            TimeUnit.SECONDS.sleep(sleepTime.toSeconds());
                        } catch (InterruptedException e) {
                            // ignore
                        }
                        writeResponse.accept("synchronized lock2.");
                    }
                }

いずれも埅぀時間は3秒にしおありたす。

            Duration sleepTime = Duration.ofSeconds(3L);

それ以倖。

                default -> writeResponse.accept("Hello World.");

たた、バむンドするアドレスやポヌトはコマンドラむン匕数やむンスタンス䜜成時に指定できるようにしおいたす。

    public static void main(String... args) {
        String host;
        int port;

        if (args.length > 1) {
            host = args[0];
            port = Integer.parseInt(args[1]);
        } else if (args.length > 0) {
            host = "localhost";
            port = Integer.parseInt(args[0]);
        } else {
            host = "localhost";
            port = 8080;
        }

        SimpleHttpServer simpleHttpServer = SimpleHttpServer.create(host, port);
        simpleHttpServer.start();

        Runtime.getRuntime().addShutdownHook(Thread.ofPlatform().unstarted(simpleHttpServer::stop));
    }

〜省略〜

    public static SimpleHttpServer create(String host, int port) {
        try {
            HttpServer httpServer = HttpServer.create(new InetSocketAddress(host, port), 0);

            log(String.format("jdk.virtualThreadScheduler.parallelism = %s", System.getProperty("jdk.virtualThreadScheduler.parallelism", "")));
            log(String.format("jdk.virtualThreadScheduler.maxPoolSize = %s", System.getProperty("jdk.virtualThreadScheduler.maxPoolSize", "")));

            // httpServer.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
            httpServer.setExecutor(Executors.newThreadPerTaskExecutor(Thread.ofVirtual().name("handler-", 1).factory()));
            httpServer.createContext("/", createHandler());

            return new SimpleHttpServer(httpServer);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public static SimpleHttpServer create(int port) {
        return create("localhost", port);
    }

䞻題はこのあたりなので、他の郚分の説明は端折りたす。

動かしお挙動を芋おみる

ビルドしお動かしおみたす。

ビルド。

$ mvn compile

実行ですが、以䞋のようにしおシステムプロパティjdk.virtualThreadScheduler.parallelismずjdk.virtualThreadScheduler.maxPoolSizeを
1にしお䞊列床および利甚可胜なプラットフォヌムスレッドの数を1にしおいたす。

$ java \
    -Djdk.virtualThreadScheduler.parallelism=1 \
    -Djdk.virtualThreadScheduler.maxPoolSize=1 \
    -cp target/classes \
    org.littlewings.virtualthreads.SimpleHttpServer

仮想スレッドがアンマりントできない凊理に入った時に、他のプラットフォヌムスレッドを䜿っお動䜜できないようにするこずがこの蚭定の
意図です。

起動時のログ。

[2023-12-12 00:12:22] - main - jdk.virtualThreadScheduler.parallelism = 1
[2023-12-12 00:12:22] - main - jdk.virtualThreadScheduler.maxPoolSize = 1
[2023-12-12 00:12:22] - main - simple http server[127.0.0.1:8080], started.

確認。

$ time curl localhost:8080
Hello World.
real    0m0.012s
user    0m0.006s
sys     0m0.005s

アクセスするず、こんな感じにログが出力されたす。

[2023-12-12 00:14:38] - handler-1 - access[GET:/] start
[2023-12-12 00:14:38] - handler-1 - access[GET:/] end

ここからは、2぀のタヌミナルを䜿っおcurlコマンドでアクセスし぀぀凊理時間を芋おいきたす。アクセスは、基本的に同時に
行っおいたす。

TimeUnit#sleepでスリヌプする、/sleepにアクセス。

## ひず぀目
$ time curl localhost:8080/sleep
sleep.
real    0m3.014s
user    0m0.006s
sys     0m0.005s


## 2぀目
$ time curl localhost:8080/sleep
sleep.
real    0m3.013s
user    0m0.006s
sys     0m0.005s

2぀のリク゚ストを凊理したしたが、䞡方ずもほが3秒で返っおきたした。

アクセスログ䞊も同じです。

[2023-12-12 00:17:06] - handler-5 - access[GET:/sleep] start
[2023-12-12 00:17:06] - handler-6 - access[GET:/sleep] start
[2023-12-12 00:17:09] - handler-5 - access[GET:/sleep] end
[2023-12-12 00:17:09] - handler-6 - access[GET:/sleep] end

空ルヌプを回しおCPUを消費する/heavy。

## ひず぀目
$ time curl localhost:8080/heavy
heavy.
real    0m3.011s
user    0m0.004s
sys     0m0.005s


## 2぀目
$ time curl localhost:8080/heavy
heavy.
real    0m5.672s
user    0m0.007s
sys     0m0.000s

片方が2倍近い時間になりたした。2倍以䞊になっおいるのは、各タヌミナルでそれぞれコマンドを起動しおるので、そのラグだず
思いたす 。

アクセスログを芋るず、片方が動いおいる間はもうひず぀が進められなくなっおいるみたいですね。

[2023-12-12 00:19:21] - handler-9 - access[GET:/heavy] start
[2023-12-12 00:19:24] - handler-9 - access[GET:/heavy] end
[2023-12-12 00:19:24] - handler-10 - access[GET:/heavy] start
[2023-12-12 00:19:27] - handler-10 - access[GET:/heavy] end

ReentrantLockを䜿う/lock。

## ひず぀目
$ time curl localhost:8080/lock
lock.
real    0m3.014s
user    0m0.006s
sys     0m0.005s


## 2぀目
$ time curl localhost:8080/lock
lock.
real    0m5.706s
user    0m0.012s
sys     0m0.001s

/heavyず同じように時間が玄2倍になりたしたが、これはそもそもロックを取っおいるのでこうなりたすよね。

アクセスログ。

[2023-12-12 00:21:23] - handler-17 - access[GET:/lock] start
[2023-12-12 00:21:23] - handler-18 - access[GET:/lock] start
[2023-12-12 00:21:26] - handler-17 - access[GET:/lock] end
[2023-12-12 00:21:29] - handler-18 - access[GET:/lock] end

では、異なるReentrantLockを䜿う/lockず/lock2ではどうでしょう。

## lock
$ time curl localhost:8080/lock
lock.
real    0m3.012s
user    0m0.005s
sys     0m0.005s


## lock2
$ time curl localhost:8080/lock2
lock2.
real    0m3.011s
user    0m0.008s
sys     0m0.000s

ロックしおいる察象が異なり、か぀スリヌプしおいるのはブロックするTimeUnit#sleepなので片方が2倍の凊理時間になるようなこずは
ありたせん。

アクセスログ。

[2023-12-12 00:23:26] - handler-21 - access[GET:/lock] start
[2023-12-12 00:23:27] - handler-22 - access[GET:/lock2] start
[2023-12-12 00:23:29] - handler-21 - access[GET:/lock] end
[2023-12-12 00:23:30] - handler-22 - access[GET:/lock2] end

synchronizedでロックを取る、/synchronized-lockを詊しおみたす。

## ひず぀目
$ time curl localhost:8080/synchronized-lock
synchronized lock.
real    0m3.012s
user    0m0.004s
sys     0m0.007s


## 2぀目
$ time curl localhost:8080/synchronized-lock
synchronized lock.
real    0m5.655s
user    0m0.010s
sys     0m0.004s

ロックを取っおいるので、片方は倍くらいの時間がかかりたすね。

[2023-12-12 00:25:26] - handler-25 - access[GET:/synchronized-lock] start
[2023-12-12 00:25:29] - handler-25 - access[GET:/synchronized-lock] end
[2023-12-12 00:25:29] - handler-26 - access[GET:/synchronized-lock] start
[2023-12-12 00:25:32] - handler-26 - access[GET:/synchronized-lock] end

では、別々のむンスタンスに察しおロックを取る/synchronized-lockず/synchronized-lock2で詊しおみたす。

## /synchronized-lock
$ time curl localhost:8080/synchronized-lock
synchronized lock.
real    0m3.014s
user    0m0.005s
sys     0m0.006s


## /synchronized-lock2
$ time curl localhost:8080/synchronized-lock2
synchronized lock2.
real    0m5.658s
user    0m0.001s
sys     0m0.010s

こちらは、異なるむンスタンスに察しおロックを取埗しおいるのに片方は倍近い時間がかかりたしたね。これがsynchonizedブロックを
䜿っおいるずアンマりントできないずいうこずなのかなず思いたす。

アクセスログ。

[2023-12-12 00:27:30] - handler-29 - access[GET:/synchronized-lock] start
[2023-12-12 00:27:33] - handler-29 - access[GET:/synchronized-lock] end
[2023-12-12 00:27:33] - handler-30 - access[GET:/synchronized-lock2] start
[2023-12-12 00:27:36] - handler-30 - access[GET:/synchronized-lock2] end

ずいうこずは、先にsychronizedブロックのようなアンマりントできないものを動かすず、アンマりント可胜な凊理でも埅たされるこずに
なるはずですね。

/synchronized-lockず/sleepで詊しおみたしょう。

## /synchronized-lock
$ time curl localhost:8080/synchronized-lock
synchronized lock.
real    0m3.011s
user    0m0.006s
sys     0m0.004s


## /sleep
$ time curl localhost:8080/sleep
sleep.
real    0m5.678s
user    0m0.005s
sys     0m0.005s

予想通りの結果になりたした。

ここたでで、以䞋の点は確認できたのではないかなず思いたす。

  • 仮想スレッドはプラットフォヌムスレッドがマりントしお駆動し、状況によっおアンマりントされる
  • マりントアンマりント
    • IOなどのブロック操䜜でアンマりントされる
    • ReentrantLockでアンマりントされる
    • synchronizedブロックメ゜ッドではアンマりントできない
    • CPUバりンドな凊理ずは盞性が悪い

なお、仮想スレッドをアンマりントできない状態であるpinningピン留めが発生した時にスタックトレヌスを出力する方法もあり、
それはこちらに曞いおいたす。

JEP 444(Virtual Threads)のpinning(ピン留め)をシステムプロパティjdk.tracePinnedThreadsによるスタックトレースの出力で確認する - CLOVER🍀

では、䞊列床ずプラットフォヌムスレッドの数を増やすずどうなるでしょうか。アンマりントできなくなっおも、増やした分くらいは
動いおくれそうな気がしたすね。

2にしお詊しおみたしょう。

$ java \
    -Djdk.virtualThreadScheduler.parallelism=2 \
    -Djdk.virtualThreadScheduler.maxPoolSize=2 \
    -cp target/classes \
    org.littlewings.virtualthreads.SimpleHttpServer
[2023-12-12 01:48:01] - main - jdk.virtualThreadScheduler.parallelism = 2
[2023-12-12 01:48:01] - main - jdk.virtualThreadScheduler.maxPoolSize = 2
[2023-12-12 01:48:01] - main - simple http server[127.0.0.1:8080], started.

先ほど、アクセス時間が倍になった組み合わせを詊しおみたす。

空ルヌプを回しおCPUを消費する/heavy。

## ひず぀目
$ time curl localhost:8080/heavy
heavy.
real    0m3.065s
user    0m0.006s
sys     0m0.000s


## 2぀目
$ time curl localhost:8080/heavy
heavy.
real    0m3.009s
user    0m0.007s
sys     0m0.000s

2぀目のリク゚ストがひず぀目のリク゚ストを埅たなくなりたしたね。

アクセスログ。

[2023-12-12 01:48:49] - handler-1 - access[GET:/heavy] start
[2023-12-12 01:48:49] - handler-2 - access[GET:/heavy] start
[2023-12-12 01:48:52] - handler-1 - access[GET:/heavy] end
[2023-12-12 01:48:52] - handler-2 - access[GET:/heavy] end

では、別々のむンスタンスに察しおロックを取る/synchronized-lockず/synchronized-lock2。

## /synchronized-lock
$ time curl localhost:8080/synchronized-lock
synchronized lock.
real    0m3.010s
user    0m0.002s
sys     0m0.005s


## /synchronized-lock2
$ time curl localhost:8080/synchronized-lock2
synchronized lock2.
real    0m3.009s
user    0m0.003s
sys     0m0.004s

こちらも同傟向になりたしたね。

アクセスログ。

[2023-12-12 01:50:55] - handler-5 - access[GET:/synchronized-lock] start
[2023-12-12 01:50:56] - handler-6 - access[GET:/synchronized-lock2] start
[2023-12-12 01:50:58] - handler-5 - access[GET:/synchronized-lock] end
[2023-12-12 01:50:59] - handler-6 - access[GET:/synchronized-lock2] end

ずいうわけで、割り圓おられるプラットフォヌムスレッドがあればそちらを䜿っおくれるこずは確認できたした。

もっずも、こういう事態そのものを避けるべきなのでしょうけどね。

新しい圢匏のスレッドダンプを芋る

次はスレッドダンプを芋おみたしょう。

䞊列床およびプラットフォヌムスレッド数を1にしお、HTTPサヌバヌを起動し盎したす。

$ java \
    -Djdk.virtualThreadScheduler.parallelism=1 \
    -Djdk.virtualThreadScheduler.maxPoolSize=1 \
    -cp target/classes \
    org.littlewings.virtualthreads.SimpleHttpServer
[2023-12-12 22:58:34] - main - jdk.virtualThreadScheduler.parallelism = 1
[2023-12-12 22:58:34] - main - jdk.virtualThreadScheduler.maxPoolSize = 1
[2023-12-12 22:58:34] - main - simple http server[127.0.0.1:8080], started.

たずはスリヌプさせおいる時に

$ curl localhost:8080/sleep

スレッドダンプを取っおみたす。

$ jcmd $(jcmd -l | grep SimpleHttpServer | cut -d' ' -f1) Thread.print

通垞、jcmd [PID] [command]ずいう指定ですが、PIDの取埗はサブシェルに任せおいたす 。
以降は$(jcmd -l | grep SimpleHttpServer | cut -d' ' -f1)ずいう指定は「JavaアプリケヌションのPIDを取埗しおいるんだ」ず思っお
芋おください。

結果。

13711:
2023-12-12 22:59:51
Full thread dump OpenJDK 64-Bit Server VM (21.0.1+12-Ubuntu-222.04 mixed mode, sharing):

Threads class SMR info:
_java_thread_list=0x00007fd224002120, length=15, elements={
0x00007fd2b0164be0, 0x00007fd2b0166280, 0x00007fd2b0167d30, 0x00007fd2b0169390,
0x00007fd2b016a950, 0x00007fd2b016c4b0, 0x00007fd2b016db90, 0x00007fd2b0183db0,
0x00007fd2b0187880, 0x00007fd2b01cc400, 0x00007fd2b01e41a0, 0x00007fd2b001ce80,
0x00007fd1f8043850, 0x00007fd204012740, 0x00007fd224000fe0
}

"Reference Handler" #9 [13873] daemon prio=10 os_prio=0 cpu=0.40ms elapsed=78.23s tid=0x00007fd2b0164be0 nid=13873 waiting on condition  [0x00007fd290405000]
   java.lang.Thread.State: RUNNABLE
        at java.lang.ref.Reference.waitForReferencePendingList(java.base@21.0.1/Native Method)
        at java.lang.ref.Reference.processPendingReferences(java.base@21.0.1/Reference.java:246)
        at java.lang.ref.Reference$ReferenceHandler.run(java.base@21.0.1/Reference.java:208)

"Finalizer" #10 [13874] daemon prio=8 os_prio=0 cpu=0.20ms elapsed=78.23s tid=0x00007fd2b0166280 nid=13874 in Object.wait()  [0x00007fd290305000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait0(java.base@21.0.1/Native Method)
        - waiting on <0x0000000717001670> (a java.lang.ref.NativeReferenceQueue$Lock)
        at java.lang.Object.wait(java.base@21.0.1/Object.java:366)
        at java.lang.Object.wait(java.base@21.0.1/Object.java:339)
        at java.lang.ref.NativeReferenceQueue.await(java.base@21.0.1/NativeReferenceQueue.java:48)
        at java.lang.ref.ReferenceQueue.remove0(java.base@21.0.1/ReferenceQueue.java:158)
        at java.lang.ref.NativeReferenceQueue.remove(java.base@21.0.1/NativeReferenceQueue.java:89)
        - locked <0x0000000717001670> (a java.lang.ref.NativeReferenceQueue$Lock)
        at java.lang.ref.Finalizer$FinalizerThread.run(java.base@21.0.1/Finalizer.java:173)

"Signal Dispatcher" #11 [13875] daemon prio=9 os_prio=0 cpu=0.33ms elapsed=78.23s tid=0x00007fd2b0167d30 nid=13875 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Service Thread" #12 [13876] daemon prio=9 os_prio=0 cpu=0.18ms elapsed=78.23s tid=0x00007fd2b0169390 nid=13876 runnable  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Monitor Deflation Thread" #13 [13877] daemon prio=9 os_prio=0 cpu=16.01ms elapsed=78.23s tid=0x00007fd2b016a950 nid=13877 runnable  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #14 [13878] daemon prio=9 os_prio=0 cpu=79.37ms elapsed=78.23s tid=0x00007fd2b016c4b0 nid=13878 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   No compile task

"C1 CompilerThread0" #17 [13879] daemon prio=9 os_prio=0 cpu=147.73ms elapsed=78.23s tid=0x00007fd2b016db90 nid=13879 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   No compile task

"Notification Thread" #18 [13880] daemon prio=9 os_prio=0 cpu=0.30ms elapsed=78.06s tid=0x00007fd2b0183db0 nid=13880 runnable  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Common-Cleaner" #19 [13881] daemon prio=8 os_prio=0 cpu=0.57ms elapsed=77.93s tid=0x00007fd2b0187880 nid=13881 waiting on condition  [0x00007fd23ab2d000]
   java.lang.Thread.State: TIMED_WAITING (parking)
        at jdk.internal.misc.Unsafe.park(java.base@21.0.1/Native Method)
        - parking to wait for  <0x0000000717011010> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.parkNanos(java.base@21.0.1/LockSupport.java:269)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@21.0.1/AbstractQueuedSynchronizer.java:1847)
        at java.lang.ref.ReferenceQueue.await(java.base@21.0.1/ReferenceQueue.java:71)
        at java.lang.ref.ReferenceQueue.remove0(java.base@21.0.1/ReferenceQueue.java:143)
        at java.lang.ref.ReferenceQueue.remove(java.base@21.0.1/ReferenceQueue.java:218)
        at jdk.internal.ref.CleanerImpl.run(java.base@21.0.1/CleanerImpl.java:140)
        at java.lang.Thread.runWith(java.base@21.0.1/Thread.java:1596)
        at java.lang.Thread.run(java.base@21.0.1/Thread.java:1583)
        at jdk.internal.misc.InnocuousThread.run(java.base@21.0.1/InnocuousThread.java:186)

"idle-timeout-task" #20 [13882] daemon prio=5 os_prio=0 cpu=1.03ms elapsed=77.68s tid=0x00007fd2b01cc400 nid=13882 in Object.wait()  [0x00007fd23aa16000]
   java.lang.Thread.State: TIMED_WAITING (on object monitor)
        at java.lang.Object.wait0(java.base@21.0.1/Native Method)
        - waiting on <0x000000071706e308> (a java.util.TaskQueue)
        at java.lang.Object.wait(java.base@21.0.1/Object.java:366)
        at java.util.TimerThread.mainLoop(java.base@21.0.1/Timer.java:563)
        - locked <0x000000071706e308> (a java.util.TaskQueue)
        at java.util.TimerThread.run(java.base@21.0.1/Timer.java:516)

"HTTP-Dispatcher" #21 [13884] prio=5 os_prio=0 cpu=50.94ms elapsed=77.59s tid=0x00007fd2b01e41a0 nid=13884 runnable  [0x00007fd23a80b000]
   java.lang.Thread.State: RUNNABLE
        at sun.nio.ch.EPoll.wait(java.base@21.0.1/Native Method)
        at sun.nio.ch.EPollSelectorImpl.doSelect(java.base@21.0.1/EPollSelectorImpl.java:121)
        at sun.nio.ch.SelectorImpl.lockAndDoSelect(java.base@21.0.1/SelectorImpl.java:130)
        - locked <0x000000071706bc48> (a sun.nio.ch.Util$2)
        - locked <0x000000071706b8c0> (a sun.nio.ch.EPollSelectorImpl)
        at sun.nio.ch.SelectorImpl.select(java.base@21.0.1/SelectorImpl.java:142)
        at sun.net.httpserver.ServerImpl$Dispatcher.run(jdk.httpserver@21.0.1/ServerImpl.java:474)
        at java.lang.Thread.runWith(java.base@21.0.1/Thread.java:1596)
        at java.lang.Thread.run(java.base@21.0.1/Thread.java:1583)

"DestroyJavaVM" #23 [13712] prio=5 os_prio=0 cpu=276.12ms elapsed=77.56s tid=0x00007fd2b001ce80 nid=13712 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"ForkJoinPool-1-worker-1" #25 [14092] daemon prio=5 os_prio=0 cpu=25.68ms elapsed=2.32s tid=0x00007fd1f8043850 nid=14092 waiting on condition  [0x00007fd23a90b000]
   java.lang.Thread.State: TIMED_WAITING (parking)
        at jdk.internal.misc.Unsafe.park(java.base@21.0.1/Native Method)
        - parking to wait for  <0x0000000716f0c408> (a java.util.concurrent.ForkJoinPool)
        at java.util.concurrent.locks.LockSupport.parkUntil(java.base@21.0.1/LockSupport.java:449)
        at java.util.concurrent.ForkJoinPool.awaitWork(java.base@21.0.1/ForkJoinPool.java:1891)
        at java.util.concurrent.ForkJoinPool.runWorker(java.base@21.0.1/ForkJoinPool.java:1809)
        at java.util.concurrent.ForkJoinWorkerThread.run(java.base@21.0.1/ForkJoinWorkerThread.java:188)

"VirtualThread-unparker" #26 [14093] daemon prio=5 os_prio=0 cpu=0.38ms elapsed=2.25s tid=0x00007fd204012740 nid=14093 waiting on condition  [0x00007fd23a70b000]
   java.lang.Thread.State: TIMED_WAITING (parking)
        at jdk.internal.misc.Unsafe.park(java.base@21.0.1/Native Method)
        - parking to wait for  <0x0000000716f14a70> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.parkNanos(java.base@21.0.1/LockSupport.java:269)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(java.base@21.0.1/AbstractQueuedSynchronizer.java:1758)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(java.base@21.0.1/ScheduledThreadPoolExecutor.java:1182)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(java.base@21.0.1/ScheduledThreadPoolExecutor.java:899)
        at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@21.0.1/ThreadPoolExecutor.java:1070)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@21.0.1/ThreadPoolExecutor.java:1130)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@21.0.1/ThreadPoolExecutor.java:642)
        at java.lang.Thread.runWith(java.base@21.0.1/Thread.java:1596)
        at java.lang.Thread.run(java.base@21.0.1/Thread.java:1583)
        at jdk.internal.misc.InnocuousThread.run(java.base@21.0.1/InnocuousThread.java:186)

"Attach Listener" #27 [14136] daemon prio=9 os_prio=0 cpu=0.30ms elapsed=0.10s tid=0x00007fd224000fe0 nid=14136 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"VM Thread" os_prio=0 cpu=4.86ms elapsed=78.45s tid=0x00007fd2b0157850 nid=13872 runnable

"GC Thread#0" os_prio=0 cpu=0.25ms elapsed=79.36s tid=0x00007fd2b0084aa0 nid=13713 runnable

"G1 Main Marker" os_prio=0 cpu=0.25ms elapsed=79.36s tid=0x00007fd2b0095950 nid=13714 runnable

"G1 Conc#0" os_prio=0 cpu=0.20ms elapsed=79.36s tid=0x00007fd2b0096910 nid=13715 runnable

"G1 Refine#0" os_prio=0 cpu=0.12ms elapsed=79.36s tid=0x00007fd2b0122430 nid=13716 runnable

"G1 Service" os_prio=0 cpu=4.68ms elapsed=79.36s tid=0x00007fd2b0123400 nid=13717 runnable

"VM Periodic Task Thread" os_prio=0 cpu=85.24ms elapsed=78.62s tid=0x00007fd2b013d0f0 nid=13867 waiting on condition

JNI global refs: 16, weak refs: 0

TimeUnit#sleepで止たっおいるスレッドがいたせん。そもそも、handler-Nずいう名前のスレッドもいたせんね。どうやら
Virtual Threadsは衚瀺されないようです。

Virtual Threadsのスタックトレヌスを含むには、Thread.dump_to_fileを䜿うようです。

$ jcmd $(jcmd -l | grep SimpleHttpServer | cut -d' ' -f1) help Thread.dump_to_file
13711:
Thread.dump_to_file
Dump threads, with stack traces, to a file in plain text or JSON format.

Impact: Medium: Depends on the number of threads.

Syntax : Thread.dump_to_file [options] <filepath>

Arguments:
        filepath :  The file path to the output file (STRING, no default value)

Options: (options must be specified using the <key> or <key>=<value> syntax)
        -overwrite : [optional] May overwrite existing file (BOOLEAN, false)
        -format : [optional] Output format ("plain" or "json") (STRING, plain)

ちなみに、このコマンドはjcmdのドキュメントには茉っおいなさそうです 。

jcmdコマンド

もう1床スリヌプさせお

$ curl localhost:8080/sleep

スレッドダンプを取埗。出力圢匏はたずはテキストplainにしおいたす。たた、このコマンドは出力結果がファむルになりたす。

$ jcmd $(jcmd -l | grep SimpleHttpServer | cut -d' ' -f1) Thread.dump_to_file -format=plain thread_dump.txt

ファむルが䜜成されたした。

13711:
Created /path/to/thread_dump.txt

䞭身を芋おみたす。

thread_dump.txt

13711
2023-12-12T14:07:43.882089998Z
21.0.1+12-Ubuntu-222.04

#9 "Reference Handler"
      java.base/java.lang.ref.Reference.waitForReferencePendingList(Native Method)
      java.base/java.lang.ref.Reference.processPendingReferences(Reference.java:246)
      java.base/java.lang.ref.Reference$ReferenceHandler.run(Reference.java:208)

#10 "Finalizer"
      java.base/java.lang.Object.wait0(Native Method)
      java.base/java.lang.Object.wait(Object.java:366)
      java.base/java.lang.Object.wait(Object.java:339)
      java.base/java.lang.ref.NativeReferenceQueue.await(NativeReferenceQueue.java:48)
      java.base/java.lang.ref.ReferenceQueue.remove0(ReferenceQueue.java:158)
      java.base/java.lang.ref.NativeReferenceQueue.remove(NativeReferenceQueue.java:89)
      java.base/java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:173)

#11 "Signal Dispatcher"

#18 "Notification Thread"

#19 "Common-Cleaner"
      java.base/jdk.internal.misc.Unsafe.park(Native Method)
      java.base/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:269)
      java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1847)
      java.base/java.lang.ref.ReferenceQueue.await(ReferenceQueue.java:71)
      java.base/java.lang.ref.ReferenceQueue.remove0(ReferenceQueue.java:143)
      java.base/java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:218)
      java.base/jdk.internal.ref.CleanerImpl.run(CleanerImpl.java:140)
      java.base/java.lang.Thread.run(Thread.java:1583)
      java.base/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:186)

#20 "idle-timeout-task"
      java.base/java.lang.Object.wait0(Native Method)
      java.base/java.lang.Object.wait(Object.java:366)
      java.base/java.util.TimerThread.mainLoop(Timer.java:563)
      java.base/java.util.TimerThread.run(Timer.java:516)

#21 "HTTP-Dispatcher"
      java.base/sun.nio.ch.EPoll.wait(Native Method)
      java.base/sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:121)
      java.base/sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:130)
      java.base/sun.nio.ch.SelectorImpl.select(SelectorImpl.java:142)
      jdk.httpserver/sun.net.httpserver.ServerImpl$Dispatcher.run(ServerImpl.java:474)
      java.base/java.lang.Thread.run(Thread.java:1583)

#23 "DestroyJavaVM"

#27 "Attach Listener"
      java.base/java.lang.Thread.getStackTrace(Thread.java:2450)
      java.base/jdk.internal.vm.ThreadDumper.dumpThread(ThreadDumper.java:162)
      java.base/jdk.internal.vm.ThreadDumper.lambda$dumpThreads$0(ThreadDumper.java:155)
      java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
      java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:1024)
      java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
      java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
      java.base/java.util.stream.StreamSpliterators$WrappingSpliterator.forEachRemaining(StreamSpliterators.java:310)
      java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:734)
      java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:762)
      java.base/jdk.internal.vm.ThreadDumper.dumpThreads(ThreadDumper.java:155)
      java.base/jdk.internal.vm.ThreadDumper.dumpThreads(ThreadDumper.java:151)
      java.base/jdk.internal.vm.ThreadDumper.dumpThreadsToFile(ThreadDumper.java:117)
      java.base/jdk.internal.vm.ThreadDumper.dumpThreads(ThreadDumper.java:67)

#29 "handler-3" virtual
      java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:621)
      java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:793)
      java.base/java.lang.Thread.sleep(Thread.java:556)
      java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
      org.littlewings.virtualthreads.SimpleHttpServer.lambda$createHandler$1(SimpleHttpServer.java:109)
      jdk.httpserver/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98)
      jdk.httpserver/sun.net.httpserver.AuthFilter.doFilter(AuthFilter.java:82)
      jdk.httpserver/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:101)
      jdk.httpserver/sun.net.httpserver.ServerImpl$Exchange$LinkHandler.handle(ServerImpl.java:871)
      jdk.httpserver/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98)
      jdk.httpserver/sun.net.httpserver.ServerImpl$Exchange.run(ServerImpl.java:847)
      java.base/java.util.concurrent.ThreadPerTaskExecutor$TaskRunner.run(ThreadPerTaskExecutor.java:314)
      java.base/java.lang.VirtualThread.run(VirtualThread.java:309)

#30 "ForkJoinPool-1-worker-2"
      java.base/jdk.internal.misc.Unsafe.park(Native Method)
      java.base/java.util.concurrent.locks.LockSupport.parkUntil(LockSupport.java:449)
      java.base/java.util.concurrent.ForkJoinPool.awaitWork(ForkJoinPool.java:1891)
      java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1809)
      java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188)

#26 "VirtualThread-unparker"
      java.base/jdk.internal.misc.Unsafe.park(Native Method)
      java.base/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:269)
      java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:1758)
      java.base/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1182)
      java.base/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:899)
      java.base/java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1070)
      java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
      java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
      java.base/java.lang.Thread.run(Thread.java:1583)
      java.base/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:186)

今床はVirtual Threadsが出珟したした。

#29 "handler-3" virtual
      java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:621)
      java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:793)
      java.base/java.lang.Thread.sleep(Thread.java:556)
      java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
      org.littlewings.virtualthreads.SimpleHttpServer.lambda$createHandler$1(SimpleHttpServer.java:109)
      jdk.httpserver/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98)
      jdk.httpserver/sun.net.httpserver.AuthFilter.doFilter(AuthFilter.java:82)
      jdk.httpserver/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:101)
      jdk.httpserver/sun.net.httpserver.ServerImpl$Exchange$LinkHandler.handle(ServerImpl.java:871)
      jdk.httpserver/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98)
      jdk.httpserver/sun.net.httpserver.ServerImpl$Exchange.run(ServerImpl.java:847)
      java.base/java.util.concurrent.ThreadPerTaskExecutor$TaskRunner.run(ThreadPerTaskExecutor.java:314)
      java.base/java.lang.VirtualThread.run(VirtualThread.java:309)

Thread.dump_to_fileはformat=jsonでJSON圢匏でも出力できたす。

$ jcmd $(jcmd -l | grep SimpleHttpServer | cut -d' ' -f1) Thread.dump_to_file -format=json thread_dump.json

結果。

thread_dump.json

{
  "threadDump": {
    "processId": "13711",
    "time": "2023-12-12T14:11:05.724062912Z",
    "runtimeVersion": "21.0.1+12-Ubuntu-222.04",
    "threadContainers": [
      {
        "container": "<root>",
        "parent": null,
        "owner": null,
        "threads": [
         {
           "tid": "9",
           "name": "Reference Handler",
           "stack": [
              "java.base\/java.lang.ref.Reference.waitForReferencePendingList(Native Method)",
              "java.base\/java.lang.ref.Reference.processPendingReferences(Reference.java:246)",
              "java.base\/java.lang.ref.Reference$ReferenceHandler.run(Reference.java:208)"
           ]
         },
         {
           "tid": "10",
           "name": "Finalizer",
           "stack": [
              "java.base\/java.lang.Object.wait0(Native Method)",
              "java.base\/java.lang.Object.wait(Object.java:366)",
              "java.base\/java.lang.Object.wait(Object.java:339)",
              "java.base\/java.lang.ref.NativeReferenceQueue.await(NativeReferenceQueue.java:48)",
              "java.base\/java.lang.ref.ReferenceQueue.remove0(ReferenceQueue.java:158)",
              "java.base\/java.lang.ref.NativeReferenceQueue.remove(NativeReferenceQueue.java:89)",
              "java.base\/java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:173)"
           ]
         },
         {
           "tid": "11",
           "name": "Signal Dispatcher",
           "stack": [
           ]
         },
         {
           "tid": "18",
           "name": "Notification Thread",
           "stack": [
           ]
         },
         {
           "tid": "19",
           "name": "Common-Cleaner",
           "stack": [
              "java.base\/jdk.internal.misc.Unsafe.park(Native Method)",
              "java.base\/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:269)",
              "java.base\/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1847)",
              "java.base\/java.lang.ref.ReferenceQueue.await(ReferenceQueue.java:71)",
              "java.base\/java.lang.ref.ReferenceQueue.remove0(ReferenceQueue.java:143)",
              "java.base\/java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:218)",
              "java.base\/jdk.internal.ref.CleanerImpl.run(CleanerImpl.java:140)",
              "java.base\/java.lang.Thread.run(Thread.java:1583)",
              "java.base\/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:186)"
           ]
         },
         {
           "tid": "20",
           "name": "idle-timeout-task",
           "stack": [
              "java.base\/java.lang.Object.wait0(Native Method)",
              "java.base\/java.lang.Object.wait(Object.java:366)",
              "java.base\/java.util.TimerThread.mainLoop(Timer.java:563)",
              "java.base\/java.util.TimerThread.run(Timer.java:516)"
           ]
         },
         {
           "tid": "21",
           "name": "HTTP-Dispatcher",
           "stack": [
              "java.base\/sun.nio.ch.EPoll.wait(Native Method)",
              "java.base\/sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:121)",
              "java.base\/sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:130)",
              "java.base\/sun.nio.ch.SelectorImpl.select(SelectorImpl.java:142)",
              "jdk.httpserver\/sun.net.httpserver.ServerImpl$Dispatcher.run(ServerImpl.java:474)",
              "java.base\/java.lang.Thread.run(Thread.java:1583)"
           ]
         },
         {
           "tid": "23",
           "name": "DestroyJavaVM",
           "stack": [
           ]
         },
         {
           "tid": "27",
           "name": "Attach Listener",
           "stack": [
              "java.base\/java.lang.Thread.getStackTrace(Thread.java:2450)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadToJson(ThreadDumper.java:264)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToJson(ThreadDumper.java:237)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToJson(ThreadDumper.java:201)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToFile(ThreadDumper.java:115)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToJson(ThreadDumper.java:84)"
           ]
         }
        ],
        "threadCount": "9"
      },
      {
        "container": "ForkJoinPool.commonPool\/jdk.internal.vm.SharedThreadContainer@7350b626",
        "parent": "<root>",
        "owner": null,
        "threads": [
        ],
        "threadCount": "0"
      },
      {
        "container": "java.util.concurrent.ThreadPoolExecutor@486cc147",
        "parent": "<root>",
        "owner": null,
        "threads": [
        ],
        "threadCount": "0"
      },
      {
        "container": "java.util.concurrent.ThreadPerTaskExecutor@45a37b8c",
        "parent": "<root>",
        "owner": null,
        "threads": [
         {
           "tid": "32",
           "name": "handler-5",
           "stack": [
              "java.base\/java.lang.VirtualThread.parkNanos(VirtualThread.java:621)",
              "java.base\/java.lang.VirtualThread.sleepNanos(VirtualThread.java:793)",
              "java.base\/java.lang.Thread.sleep(Thread.java:556)",
              "java.base\/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)",
              "org.littlewings.virtualthreads.SimpleHttpServer.lambda$createHandler$1(SimpleHttpServer.java:109)",
              "jdk.httpserver\/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98)",
              "jdk.httpserver\/sun.net.httpserver.AuthFilter.doFilter(AuthFilter.java:82)",
              "jdk.httpserver\/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:101)",
              "jdk.httpserver\/sun.net.httpserver.ServerImpl$Exchange$LinkHandler.handle(ServerImpl.java:871)",
              "jdk.httpserver\/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98)",
              "jdk.httpserver\/sun.net.httpserver.ServerImpl$Exchange.run(ServerImpl.java:847)",
              "java.base\/java.util.concurrent.ThreadPerTaskExecutor$TaskRunner.run(ThreadPerTaskExecutor.java:314)",
              "java.base\/java.lang.VirtualThread.run(VirtualThread.java:309)"
           ]
         }
        ],
        "threadCount": "1"
      },
      {
        "container": "ForkJoinPool-1\/jdk.internal.vm.SharedThreadContainer@a5da302",
        "parent": "<root>",
        "owner": null,
        "threads": [
         {
           "tid": "33",
           "name": "ForkJoinPool-1-worker-3",
           "stack": [
              "java.base\/jdk.internal.misc.Unsafe.park(Native Method)",
              "java.base\/java.util.concurrent.locks.LockSupport.parkUntil(LockSupport.java:449)",
              "java.base\/java.util.concurrent.ForkJoinPool.awaitWork(ForkJoinPool.java:1891)",
              "java.base\/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1809)",
              "java.base\/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188)"
           ]
         }
        ],
        "threadCount": "1"
      },
      {
        "container": "java.util.concurrent.ScheduledThreadPoolExecutor@4d8bd5e9",
        "parent": "<root>",
        "owner": null,
        "threads": [
         {
           "tid": "26",
           "name": "VirtualThread-unparker",
           "stack": [
              "java.base\/jdk.internal.misc.Unsafe.park(Native Method)",
              "java.base\/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:269)",
              "java.base\/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:1758)",
              "java.base\/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1182)",
              "java.base\/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:899)",
              "java.base\/java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1070)",
              "java.base\/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)",
              "java.base\/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)",
              "java.base\/java.lang.Thread.run(Thread.java:1583)",
              "java.base\/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:186)"
           ]
         }
        ],
        "threadCount": "1"
      }
    ]
  }
}

Virtual Threadsはこの郚分ですね。

      {
        "container": "java.util.concurrent.ThreadPerTaskExecutor@45a37b8c",
        "parent": "<root>",
        "owner": null,
        "threads": [
         {
           "tid": "32",
           "name": "handler-5",
           "stack": [
              "java.base\/java.lang.VirtualThread.parkNanos(VirtualThread.java:621)",
              "java.base\/java.lang.VirtualThread.sleepNanos(VirtualThread.java:793)",
              "java.base\/java.lang.Thread.sleep(Thread.java:556)",
              "java.base\/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)",
              "org.littlewings.virtualthreads.SimpleHttpServer.lambda$createHandler$1(SimpleHttpServer.java:109)",
              "jdk.httpserver\/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98)",
              "jdk.httpserver\/sun.net.httpserver.AuthFilter.doFilter(AuthFilter.java:82)",
              "jdk.httpserver\/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:101)",
              "jdk.httpserver\/sun.net.httpserver.ServerImpl$Exchange$LinkHandler.handle(ServerImpl.java:871)",
              "jdk.httpserver\/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98)",
              "jdk.httpserver\/sun.net.httpserver.ServerImpl$Exchange.run(ServerImpl.java:847)",
              "java.base\/java.util.concurrent.ThreadPerTaskExecutor$TaskRunner.run(ThreadPerTaskExecutor.java:314)",
              "java.base\/java.lang.VirtualThread.run(VirtualThread.java:309)"
           ]
         }
        ],
        "threadCount": "1"
      },

JSON圢匏なのでちょっずビックリしたすが、Virtual Threadsがどこに属しおいるかなどがわかっお良いですね。

-formatを指定しない堎合は、plainになりたす。

$ jcmd $(jcmd -l | grep SimpleHttpServer | cut -d' ' -f1) Thread.dump_to_file thread_dump.txt

たた、デフォルトでは出力先にすでにファむルがある堎合は䞊曞きしたせん。

$ jcmd $(jcmd -l | grep SimpleHttpServer | cut -d' ' -f1) Thread.dump_to_file thread_dump.txt
13711:
/path/to/thread_dump.txt exists, use -overwrite to overwrite

存圚するファむルを䞊曞きする堎合は、-overwriteオプションを指定したす。

$ jcmd $(jcmd -l | grep SimpleHttpServer | cut -d' ' -f1) Thread.dump_to_file -overwrite thread_dump.txt
13711:
Created /path/to/thread_dump.txt

他もいく぀か芋おみたしょう。

synchronizedを䜿った堎合。

$ curl localhost:8080/synchronized-lock

スレッドダンプを取埗しお

$ jcmd $(jcmd -l | grep SimpleHttpServer | cut -d' ' -f1) Thread.dump_to_file -format=json -overwrite thread_dump.json

確認。

{
  "threadDump": {
    "processId": "13711",
    "time": "2023-12-12T14:22:17.829867481Z",
    "runtimeVersion": "21.0.1+12-Ubuntu-222.04",
    "threadContainers": [
      {
        "container": "<root>",
        "parent": null,
        "owner": null,
        "threads": [
         {
           "tid": "9",
           "name": "Reference Handler",
           "stack": [
              "java.base\/java.lang.ref.Reference.waitForReferencePendingList(Native Method)",
              "java.base\/java.lang.ref.Reference.processPendingReferences(Reference.java:246)",
              "java.base\/java.lang.ref.Reference$ReferenceHandler.run(Reference.java:208)"
           ]
         },
         {
           "tid": "10",
           "name": "Finalizer",
           "stack": [
              "java.base\/java.lang.Object.wait0(Native Method)",
              "java.base\/java.lang.Object.wait(Object.java:366)",
              "java.base\/java.lang.Object.wait(Object.java:339)",
              "java.base\/java.lang.ref.NativeReferenceQueue.await(NativeReferenceQueue.java:48)",
              "java.base\/java.lang.ref.ReferenceQueue.remove0(ReferenceQueue.java:158)",
              "java.base\/java.lang.ref.NativeReferenceQueue.remove(NativeReferenceQueue.java:89)",
              "java.base\/java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:173)"
           ]
         },
         {
           "tid": "11",
           "name": "Signal Dispatcher",
           "stack": [
           ]
         },
         {
           "tid": "18",
           "name": "Notification Thread",
           "stack": [
           ]
         },
         {
           "tid": "19",
           "name": "Common-Cleaner",
           "stack": [
              "java.base\/jdk.internal.misc.Unsafe.park(Native Method)",
              "java.base\/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:269)",
              "java.base\/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1847)",
              "java.base\/java.lang.ref.ReferenceQueue.await(ReferenceQueue.java:71)",
              "java.base\/java.lang.ref.ReferenceQueue.remove0(ReferenceQueue.java:143)",
              "java.base\/java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:218)",
              "java.base\/jdk.internal.ref.CleanerImpl.run(CleanerImpl.java:140)",
              "java.base\/java.lang.Thread.run(Thread.java:1583)",
              "java.base\/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:186)"
           ]
         },
         {
           "tid": "20",
           "name": "idle-timeout-task",
           "stack": [
              "java.base\/java.lang.Object.wait0(Native Method)",
              "java.base\/java.lang.Object.wait(Object.java:366)",
              "java.base\/java.util.TimerThread.mainLoop(Timer.java:563)",
              "java.base\/java.util.TimerThread.run(Timer.java:516)"
           ]
         },
         {
           "tid": "21",
           "name": "HTTP-Dispatcher",
           "stack": [
              "java.base\/sun.nio.ch.EPoll.wait(Native Method)",
              "java.base\/sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:121)",
              "java.base\/sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:130)",
              "java.base\/sun.nio.ch.SelectorImpl.select(SelectorImpl.java:142)",
              "jdk.httpserver\/sun.net.httpserver.ServerImpl$Dispatcher.run(ServerImpl.java:474)",
              "java.base\/java.lang.Thread.run(Thread.java:1583)"
           ]
         },
         {
           "tid": "23",
           "name": "DestroyJavaVM",
           "stack": [
           ]
         },
         {
           "tid": "27",
           "name": "Attach Listener",
           "stack": [
              "java.base\/java.lang.Thread.getStackTrace(Thread.java:2450)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadToJson(ThreadDumper.java:264)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToJson(ThreadDumper.java:237)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToJson(ThreadDumper.java:201)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToFile(ThreadDumper.java:115)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToJson(ThreadDumper.java:84)"
           ]
         }
        ],
        "threadCount": "9"
      },
      {
        "container": "ForkJoinPool.commonPool\/jdk.internal.vm.SharedThreadContainer@7350b626",
        "parent": "<root>",
        "owner": null,
        "threads": [
        ],
        "threadCount": "0"
      },
      {
        "container": "java.util.concurrent.ThreadPoolExecutor@486cc147",
        "parent": "<root>",
        "owner": null,
        "threads": [
        ],
        "threadCount": "0"
      },
      {
        "container": "java.util.concurrent.ThreadPerTaskExecutor@45a37b8c",
        "parent": "<root>",
        "owner": null,
        "threads": [
         {
           "tid": "41",
           "name": "handler-11",
           "stack": [
              "java.base\/jdk.internal.misc.Unsafe.park(Native Method)",
              "java.base\/java.lang.VirtualThread.parkOnCarrierThread(VirtualThread.java:665)",
              "java.base\/java.lang.VirtualThread.parkNanos(VirtualThread.java:636)",
              "java.base\/java.lang.VirtualThread.sleepNanos(VirtualThread.java:793)",
              "java.base\/java.lang.Thread.sleep(Thread.java:556)",
              "java.base\/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)",
              "org.littlewings.virtualthreads.SimpleHttpServer.lambda$createHandler$1(SimpleHttpServer.java:159)",
              "jdk.httpserver\/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98)",
              "jdk.httpserver\/sun.net.httpserver.AuthFilter.doFilter(AuthFilter.java:82)",
              "jdk.httpserver\/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:101)",
              "jdk.httpserver\/sun.net.httpserver.ServerImpl$Exchange$LinkHandler.handle(ServerImpl.java:871)",
              "jdk.httpserver\/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98)",
              "jdk.httpserver\/sun.net.httpserver.ServerImpl$Exchange.run(ServerImpl.java:847)",
              "java.base\/java.util.concurrent.ThreadPerTaskExecutor$TaskRunner.run(ThreadPerTaskExecutor.java:314)",
              "java.base\/java.lang.VirtualThread.run(VirtualThread.java:309)"
           ]
         }
        ],
        "threadCount": "1"
      },
      {
        "container": "ForkJoinPool-1\/jdk.internal.vm.SharedThreadContainer@a5da302",
        "parent": "<root>",
        "owner": null,
        "threads": [
         {
           "tid": "42",
           "name": "ForkJoinPool-1-worker-6",
           "stack": [
              "java.base\/jdk.internal.vm.Continuation.run(Continuation.java:248)",
              "java.base\/java.lang.VirtualThread.runContinuation(VirtualThread.java:221)",
              "java.base\/java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1423)",
              "java.base\/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:387)",
              "java.base\/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1312)",
              "java.base\/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1843)",
              "java.base\/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1808)",
              "java.base\/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188)"
           ]
         }
        ],
        "threadCount": "1"
      },
      {
        "container": "java.util.concurrent.ScheduledThreadPoolExecutor@4d8bd5e9",
        "parent": "<root>",
        "owner": null,
        "threads": [
         {
           "tid": "26",
           "name": "VirtualThread-unparker",
           "stack": [
              "java.base\/jdk.internal.misc.Unsafe.park(Native Method)",
              "java.base\/java.util.concurrent.locks.LockSupport.park(LockSupport.java:371)",
              "java.base\/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(AbstractQueuedSynchronizer.java:519)",
              "java.base\/java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3780)",
              "java.base\/java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3725)",
              "java.base\/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1707)",
              "java.base\/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1170)",
              "java.base\/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:899)",
              "java.base\/java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1070)",
              "java.base\/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)",
              "java.base\/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)",
              "java.base\/java.lang.Thread.run(Thread.java:1583)",
              "java.base\/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:186)"
           ]
         }
        ],
        "threadCount": "1"
      }
    ]
  }
}

以降はスレッドダンプを取埗する箇所は省略したす。

ReentrantLock。

$ curl localhost:8080/lock

結果。

{
  "threadDump": {
    "processId": "13711",
    "time": "2023-12-12T14:23:14.386811163Z",
    "runtimeVersion": "21.0.1+12-Ubuntu-222.04",
    "threadContainers": [
      {
        "container": "<root>",
        "parent": null,
        "owner": null,
        "threads": [
         {
           "tid": "9",
           "name": "Reference Handler",
           "stack": [
              "java.base\/java.lang.ref.Reference.waitForReferencePendingList(Native Method)",
              "java.base\/java.lang.ref.Reference.processPendingReferences(Reference.java:246)",
              "java.base\/java.lang.ref.Reference$ReferenceHandler.run(Reference.java:208)"
           ]
         },
         {
           "tid": "10",
           "name": "Finalizer",
           "stack": [
              "java.base\/java.lang.Object.wait0(Native Method)",
              "java.base\/java.lang.Object.wait(Object.java:366)",
              "java.base\/java.lang.Object.wait(Object.java:339)",
              "java.base\/java.lang.ref.NativeReferenceQueue.await(NativeReferenceQueue.java:48)",
              "java.base\/java.lang.ref.ReferenceQueue.remove0(ReferenceQueue.java:158)",
              "java.base\/java.lang.ref.NativeReferenceQueue.remove(NativeReferenceQueue.java:89)",
              "java.base\/java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:173)"
           ]
         },
         {
           "tid": "11",
           "name": "Signal Dispatcher",
           "stack": [
           ]
         },
         {
           "tid": "18",
           "name": "Notification Thread",
           "stack": [
           ]
         },
         {
           "tid": "19",
           "name": "Common-Cleaner",
           "stack": [
              "java.base\/jdk.internal.misc.Unsafe.park(Native Method)",
              "java.base\/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:269)",
              "java.base\/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1847)",
              "java.base\/java.lang.ref.ReferenceQueue.await(ReferenceQueue.java:71)",
              "java.base\/java.lang.ref.ReferenceQueue.remove0(ReferenceQueue.java:143)",
              "java.base\/java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:218)",
              "java.base\/jdk.internal.ref.CleanerImpl.run(CleanerImpl.java:140)",
              "java.base\/java.lang.Thread.run(Thread.java:1583)",
              "java.base\/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:186)"
           ]
         },
         {
           "tid": "20",
           "name": "idle-timeout-task",
           "stack": [
              "java.base\/java.lang.Object.wait0(Native Method)",
              "java.base\/java.lang.Object.wait(Object.java:366)",
              "java.base\/java.util.TimerThread.mainLoop(Timer.java:563)",
              "java.base\/java.util.TimerThread.run(Timer.java:516)"
           ]
         },
         {
           "tid": "21",
           "name": "HTTP-Dispatcher",
           "stack": [
              "java.base\/sun.nio.ch.EPoll.wait(Native Method)",
              "java.base\/sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:121)",
              "java.base\/sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:130)",
              "java.base\/sun.nio.ch.SelectorImpl.select(SelectorImpl.java:142)",
              "jdk.httpserver\/sun.net.httpserver.ServerImpl$Dispatcher.run(ServerImpl.java:474)",
              "java.base\/java.lang.Thread.run(Thread.java:1583)"
           ]
         },
         {
           "tid": "23",
           "name": "DestroyJavaVM",
           "stack": [
           ]
         },
         {
           "tid": "27",
           "name": "Attach Listener",
           "stack": [
              "java.base\/java.lang.Thread.getStackTrace(Thread.java:2450)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadToJson(ThreadDumper.java:264)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToJson(ThreadDumper.java:237)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToJson(ThreadDumper.java:201)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToFile(ThreadDumper.java:115)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToJson(ThreadDumper.java:84)"
           ]
         }
        ],
        "threadCount": "9"
      },
      {
        "container": "ForkJoinPool.commonPool\/jdk.internal.vm.SharedThreadContainer@7350b626",
        "parent": "<root>",
        "owner": null,
        "threads": [
        ],
        "threadCount": "0"
      },
      {
        "container": "java.util.concurrent.ThreadPoolExecutor@486cc147",
        "parent": "<root>",
        "owner": null,
        "threads": [
        ],
        "threadCount": "0"
      },
      {
        "container": "java.util.concurrent.ThreadPerTaskExecutor@45a37b8c",
        "parent": "<root>",
        "owner": null,
        "threads": [
         {
           "tid": "44",
           "name": "handler-13",
           "stack": [
              "java.base\/java.lang.VirtualThread.parkNanos(VirtualThread.java:621)",
              "java.base\/java.lang.VirtualThread.sleepNanos(VirtualThread.java:793)",
              "java.base\/java.lang.Thread.sleep(Thread.java:556)",
              "java.base\/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)",
              "org.littlewings.virtualthreads.SimpleHttpServer.lambda$createHandler$1(SimpleHttpServer.java:136)",
              "jdk.httpserver\/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98)",
              "jdk.httpserver\/sun.net.httpserver.AuthFilter.doFilter(AuthFilter.java:82)",
              "jdk.httpserver\/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:101)",
              "jdk.httpserver\/sun.net.httpserver.ServerImpl$Exchange$LinkHandler.handle(ServerImpl.java:871)",
              "jdk.httpserver\/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98)",
              "jdk.httpserver\/sun.net.httpserver.ServerImpl$Exchange.run(ServerImpl.java:847)",
              "java.base\/java.util.concurrent.ThreadPerTaskExecutor$TaskRunner.run(ThreadPerTaskExecutor.java:314)",
              "java.base\/java.lang.VirtualThread.run(VirtualThread.java:309)"
           ]
         }
        ],
        "threadCount": "1"
      },
      {
        "container": "ForkJoinPool-1\/jdk.internal.vm.SharedThreadContainer@a5da302",
        "parent": "<root>",
        "owner": null,
        "threads": [
         {
           "tid": "45",
           "name": "ForkJoinPool-1-worker-7",
           "stack": [
              "java.base\/jdk.internal.misc.Unsafe.park(Native Method)",
              "java.base\/java.util.concurrent.locks.LockSupport.parkUntil(LockSupport.java:449)",
              "java.base\/java.util.concurrent.ForkJoinPool.awaitWork(ForkJoinPool.java:1891)",
              "java.base\/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1809)",
              "java.base\/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188)"
           ]
         }
        ],
        "threadCount": "1"
      },
      {
        "container": "java.util.concurrent.ScheduledThreadPoolExecutor@4d8bd5e9",
        "parent": "<root>",
        "owner": null,
        "threads": [
         {
           "tid": "26",
           "name": "VirtualThread-unparker",
           "stack": [
              "java.base\/jdk.internal.misc.Unsafe.park(Native Method)",
              "java.base\/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:269)",
              "java.base\/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:1758)",
              "java.base\/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1182)",
              "java.base\/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:899)",
              "java.base\/java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1070)",
              "java.base\/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)",
              "java.base\/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)",
              "java.base\/java.lang.Thread.run(Thread.java:1583)",
              "java.base\/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:186)"
           ]
         }
        ],
        "threadCount": "1"
      }
    ]
  }
}

あたり面癜い結果ではないかもしれたせんね 。

CPUを消費する凊理。

$ curl localhost:8080/heavy

スレッドダンプ。

{
  "threadDump": {
    "processId": "13711",
    "time": "2023-12-12T14:23:59.202042310Z",
    "runtimeVersion": "21.0.1+12-Ubuntu-222.04",
    "threadContainers": [
      {
        "container": "<root>",
        "parent": null,
        "owner": null,
        "threads": [
         {
           "tid": "9",
           "name": "Reference Handler",
           "stack": [
              "java.base\/java.lang.ref.Reference.waitForReferencePendingList(Native Method)",
              "java.base\/java.lang.ref.Reference.processPendingReferences(Reference.java:246)",
              "java.base\/java.lang.ref.Reference$ReferenceHandler.run(Reference.java:208)"
           ]
         },
         {
           "tid": "10",
           "name": "Finalizer",
           "stack": [
              "java.base\/java.lang.Object.wait0(Native Method)",
              "java.base\/java.lang.Object.wait(Object.java:366)",
              "java.base\/java.lang.Object.wait(Object.java:339)",
              "java.base\/java.lang.ref.NativeReferenceQueue.await(NativeReferenceQueue.java:48)",
              "java.base\/java.lang.ref.ReferenceQueue.remove0(ReferenceQueue.java:158)",
              "java.base\/java.lang.ref.NativeReferenceQueue.remove(NativeReferenceQueue.java:89)",
              "java.base\/java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:173)"
           ]
         },
         {
           "tid": "11",
           "name": "Signal Dispatcher",
           "stack": [
           ]
         },
         {
           "tid": "18",
           "name": "Notification Thread",
           "stack": [
           ]
         },
         {
           "tid": "19",
           "name": "Common-Cleaner",
           "stack": [
              "java.base\/jdk.internal.misc.Unsafe.park(Native Method)",
              "java.base\/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:269)",
              "java.base\/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1847)",
              "java.base\/java.lang.ref.ReferenceQueue.await(ReferenceQueue.java:71)",
              "java.base\/java.lang.ref.ReferenceQueue.remove0(ReferenceQueue.java:143)",
              "java.base\/java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:218)",
              "java.base\/jdk.internal.ref.CleanerImpl.run(CleanerImpl.java:140)",
              "java.base\/java.lang.Thread.run(Thread.java:1583)",
              "java.base\/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:186)"
           ]
         },
         {
           "tid": "20",
           "name": "idle-timeout-task",
           "stack": [
              "java.base\/java.lang.Object.wait0(Native Method)",
              "java.base\/java.lang.Object.wait(Object.java:366)",
              "java.base\/java.util.TimerThread.mainLoop(Timer.java:563)",
              "java.base\/java.util.TimerThread.run(Timer.java:516)"
           ]
         },
         {
           "tid": "21",
           "name": "HTTP-Dispatcher",
           "stack": [
              "java.base\/sun.nio.ch.EPoll.wait(Native Method)",
              "java.base\/sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:121)",
              "java.base\/sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:130)",
              "java.base\/sun.nio.ch.SelectorImpl.select(SelectorImpl.java:142)",
              "jdk.httpserver\/sun.net.httpserver.ServerImpl$Dispatcher.run(ServerImpl.java:474)",
              "java.base\/java.lang.Thread.run(Thread.java:1583)"
           ]
         },
         {
           "tid": "23",
           "name": "DestroyJavaVM",
           "stack": [
           ]
         },
         {
           "tid": "27",
           "name": "Attach Listener",
           "stack": [
              "java.base\/java.lang.Thread.getStackTrace(Thread.java:2450)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadToJson(ThreadDumper.java:264)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToJson(ThreadDumper.java:237)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToJson(ThreadDumper.java:201)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToFile(ThreadDumper.java:115)",
              "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToJson(ThreadDumper.java:84)"
           ]
         }
        ],
        "threadCount": "9"
      },
      {
        "container": "ForkJoinPool.commonPool\/jdk.internal.vm.SharedThreadContainer@7350b626",
        "parent": "<root>",
        "owner": null,
        "threads": [
        ],
        "threadCount": "0"
      },
      {
        "container": "java.util.concurrent.ThreadPoolExecutor@486cc147",
        "parent": "<root>",
        "owner": null,
        "threads": [
        ],
        "threadCount": "0"
      },
      {
        "container": "java.util.concurrent.ThreadPerTaskExecutor@45a37b8c",
        "parent": "<root>",
        "owner": null,
        "threads": [
         {
           "tid": "47",
           "name": "handler-15",
           "stack": [
              "java.base\/java.time.Duration.toMillis(Duration.java:1240)",
              "org.littlewings.virtualthreads.SimpleHttpServer.lambda$createHandler$1(SimpleHttpServer.java:125)",
              "jdk.httpserver\/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98)",
              "jdk.httpserver\/sun.net.httpserver.AuthFilter.doFilter(AuthFilter.java:82)",
              "jdk.httpserver\/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:101)",
              "jdk.httpserver\/sun.net.httpserver.ServerImpl$Exchange$LinkHandler.handle(ServerImpl.java:871)",
              "jdk.httpserver\/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98)",
              "jdk.httpserver\/sun.net.httpserver.ServerImpl$Exchange.run(ServerImpl.java:847)",
              "java.base\/java.util.concurrent.ThreadPerTaskExecutor$TaskRunner.run(ThreadPerTaskExecutor.java:314)",
              "java.base\/java.lang.VirtualThread.run(VirtualThread.java:309)"
           ]
         }
        ],
        "threadCount": "1"
      },
      {
        "container": "ForkJoinPool-1\/jdk.internal.vm.SharedThreadContainer@a5da302",
        "parent": "<root>",
        "owner": null,
        "threads": [
         {
           "tid": "48",
           "name": "ForkJoinPool-1-worker-8",
           "stack": [
              "java.base\/jdk.internal.vm.Continuation.run(Continuation.java:248)",
              "java.base\/java.lang.VirtualThread.runContinuation(VirtualThread.java:221)",
              "java.base\/java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1423)",
              "java.base\/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:387)",
              "java.base\/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1312)",
              "java.base\/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1843)",
              "java.base\/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1808)",
              "java.base\/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188)"
           ]
         }
        ],
        "threadCount": "1"
      },
      {
        "container": "java.util.concurrent.ScheduledThreadPoolExecutor@4d8bd5e9",
        "parent": "<root>",
        "owner": null,
        "threads": [
         {
           "tid": "26",
           "name": "VirtualThread-unparker",
           "stack": [
              "java.base\/jdk.internal.misc.Unsafe.park(Native Method)",
              "java.base\/java.util.concurrent.locks.LockSupport.park(LockSupport.java:371)",
              "java.base\/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(AbstractQueuedSynchronizer.java:519)",
              "java.base\/java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3780)",
              "java.base\/java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3725)",
              "java.base\/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1707)",
              "java.base\/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1170)",
              "java.base\/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:899)",
              "java.base\/java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1070)",
              "java.base\/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)",
              "java.base\/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)",
              "java.base\/java.lang.Thread.run(Thread.java:1583)",
              "java.base\/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:186)"
           ]
         }
        ],
        "threadCount": "1"
      }
    ]
  }
}

スレッドダンプはこれくらいにしおおきたしょう。

HttpClientでテストしおみる

ここたでの内容を、HttpClientを䜿っおテストしおみたす。

HttpClient (Java SE 21 & JDK 21)

䜜成したテストはこちら。

src/test/java/org/littlewings/virtualthreads/SimpleHttpServerTest.java

package org.littlewings.virtualthreads;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.IntStream;

import static org.assertj.core.api.Assertions.assertThat;

class SimpleHttpServerTest {
    static {
        System.setProperty("jdk.virtualThreadScheduler.parallelism", "1");
        System.setProperty("jdk.virtualThreadScheduler.maxPoolSize", "1");
    }

    SimpleHttpServer simpleHttpServer;

    @BeforeEach
    void setUp() {
        simpleHttpServer = SimpleHttpServer.create(18080);
        simpleHttpServer.start();
    }

    @AfterEach
    void tearDown() {
        simpleHttpServer.stop();
    }

    @Test
    void simple() throws InterruptedException, ExecutionException {
        try (HttpClient httpClient = HttpClient.newBuilder().build()) {
            long startTime = 0;
            long elapsedTime = 0;

            // simple
            CompletableFuture<HttpResponse<String>> simpleResponse =
                    httpClient.sendAsync(
                            HttpRequest.newBuilder().uri(URI.create("http://localhost:18080/")).GET().build(),
                            HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
                    );

            assertThat(simpleResponse.get().body()).isEqualTo("Hello World.");

            // sleep
            startTime = System.currentTimeMillis();

            CompletableFuture<HttpResponse<String>> sleepResponse =
                    httpClient.sendAsync(
                            HttpRequest.newBuilder().uri(URI.create("http://localhost:18080/sleep")).GET().build(),
                            HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
                    );

            assertThat(sleepResponse.get().body()).isEqualTo("sleep.");

            elapsedTime = System.currentTimeMillis() - startTime;
            assertThat(elapsedTime).isGreaterThan(3000L);

            // cpu heavy
            startTime = System.currentTimeMillis();

            CompletableFuture<HttpResponse<String>> heavyResponse =
                    httpClient.sendAsync(
                            HttpRequest.newBuilder().uri(URI.create("http://localhost:18080/heavy")).GET().build(),
                            HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
                    );

            assertThat(heavyResponse.get().body()).isEqualTo("heavy.");

            elapsedTime = System.currentTimeMillis() - startTime;
            assertThat(elapsedTime).isGreaterThan(3000L);

            // lock
            startTime = System.currentTimeMillis();

            CompletableFuture<HttpResponse<String>> lockResponse =
                    httpClient.sendAsync(
                            HttpRequest.newBuilder().uri(URI.create("http://localhost:18080/lock")).GET().build(),
                            HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
                    );

            assertThat(lockResponse.get().body()).isEqualTo("lock.");

            elapsedTime = System.currentTimeMillis() - startTime;
            assertThat(elapsedTime).isGreaterThan(3000L);

            // synchronized lock
            startTime = System.currentTimeMillis();

            CompletableFuture<HttpResponse<String>> synchronizedLockResponse =
                    httpClient.sendAsync(
                            HttpRequest.newBuilder().uri(URI.create("http://localhost:18080/synchronized-lock")).GET().build(),
                            HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
                    );

            assertThat(synchronizedLockResponse.get().body()).isEqualTo("synchronized lock.");

            elapsedTime = System.currentTimeMillis() - startTime;
            assertThat(elapsedTime).isGreaterThan(3000L);
        }
    }

    @Test
    void concurrentCallSleepEndpoint() throws ExecutionException, InterruptedException {
        try (HttpClient httpClient = HttpClient.newBuilder().build()) {
            long startTime = System.currentTimeMillis();

            List<CompletableFuture<HttpResponse<String>>> responses =
                    IntStream
                            .rangeClosed(1, 3)
                            .mapToObj(i ->
                                    httpClient.sendAsync(
                                            HttpRequest.newBuilder().uri(URI.create("http://localhost:18080/sleep")).GET().build(),
                                            HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
                                    )
                            )
                            .toList();

            for (CompletableFuture<HttpResponse<String>> response : responses) {
                assertThat(response.get().body()).isEqualTo("sleep.");
            }

            long elapsedTime = System.currentTimeMillis() - startTime;
            assertThat(elapsedTime).isGreaterThan(3000L);
            assertThat(elapsedTime).isLessThan(4000L);
        }
    }

    @Test
    void concurrentCallCpuHeavyEndpoint() throws ExecutionException, InterruptedException {
        try (HttpClient httpClient = HttpClient.newBuilder().build()) {
            long startTime = System.currentTimeMillis();

            List<CompletableFuture<HttpResponse<String>>> responses =
                    IntStream
                            .rangeClosed(1, 3)
                            .mapToObj(i ->
                                    httpClient.sendAsync(
                                            HttpRequest.newBuilder().uri(URI.create("http://localhost:18080/heavy")).GET().build(),
                                            HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
                                    )
                            )
                            .toList();

            for (CompletableFuture<HttpResponse<String>> response : responses) {
                assertThat(response.get().body()).isEqualTo("heavy.");
            }

            long elapsedTime = System.currentTimeMillis() - startTime;
            assertThat(elapsedTime).isGreaterThan(9000L);
            assertThat(elapsedTime).isLessThan(10000L);
        }
    }

    @Test
    void concurrentCallLockEndpoint() throws ExecutionException, InterruptedException {
        try (HttpClient httpClient = HttpClient.newBuilder().build()) {
            long startTime = System.currentTimeMillis();

            List<CompletableFuture<HttpResponse<String>>> responses =
                    IntStream
                            .rangeClosed(1, 3)
                            .mapToObj(i ->
                                    httpClient.sendAsync(
                                            HttpRequest.newBuilder().uri(URI.create("http://localhost:18080/lock")).GET().build(),
                                            HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
                                    )
                            )
                            .toList();

            for (CompletableFuture<HttpResponse<String>> response : responses) {
                assertThat(response.get().body()).isEqualTo("lock.");
            }

            long elapsedTime = System.currentTimeMillis() - startTime;
            assertThat(elapsedTime).isGreaterThan(9000L);
            assertThat(elapsedTime).isLessThan(10000L);
        }
    }

    @Test
    void concurrentCallSynchronizedLockEndpoint() throws ExecutionException, InterruptedException {
        try (HttpClient httpClient = HttpClient.newBuilder().build()) {
            long startTime = System.currentTimeMillis();

            List<CompletableFuture<HttpResponse<String>>> responses =
                    IntStream
                            .rangeClosed(1, 3)
                            .mapToObj(i ->
                                    httpClient.sendAsync(
                                            HttpRequest.newBuilder().uri(URI.create("http://localhost:18080/synchronized-lock")).GET().build(),
                                            HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
                                    )
                            )
                            .toList();

            for (CompletableFuture<HttpResponse<String>> response : responses) {
                assertThat(response.get().body()).isEqualTo("synchronized lock.");
            }

            long elapsedTime = System.currentTimeMillis() - startTime;
            assertThat(elapsedTime).isGreaterThan(9000L);
            assertThat(elapsedTime).isLessThan(10000L);
        }
    }

    @Test
    void concurrentCallLockMixedEndpoint() throws ExecutionException, InterruptedException {
        try (HttpClient httpClient = HttpClient.newBuilder().build()) {
            long startTime = System.currentTimeMillis();

            List<CompletableFuture<HttpResponse<String>>> responses =
                    List.of(
                            httpClient.sendAsync(
                                    HttpRequest.newBuilder().uri(URI.create("http://localhost:18080/lock")).GET().build(),
                                    HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
                            ),
                            httpClient.sendAsync(
                                    HttpRequest.newBuilder().uri(URI.create("http://localhost:18080/lock2")).GET().build(),
                                    HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
                            )
                    );

            assertThat(responses.get(0).get().body()).isEqualTo("lock.");
            assertThat(responses.get(1).get().body()).isEqualTo("lock2.");

            long elapsedTime = System.currentTimeMillis() - startTime;
            assertThat(elapsedTime).isGreaterThan(3000L);
            assertThat(elapsedTime).isLessThan(4000L);
        }
    }

    @Test
    void concurrentCallSynchronizedLockMixedEndpoint() throws ExecutionException, InterruptedException {
        try (HttpClient httpClient = HttpClient.newBuilder().build()) {
            long startTime = System.currentTimeMillis();

            List<CompletableFuture<HttpResponse<String>>> responses =
                    List.of(
                            httpClient.sendAsync(
                                    HttpRequest.newBuilder().uri(URI.create("http://localhost:18080/synchronized-lock")).GET().build(),
                                    HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
                            ),
                            httpClient.sendAsync(
                                    HttpRequest.newBuilder().uri(URI.create("http://localhost:18080/synchronized-lock2")).GET().build(),
                                    HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
                            )
                    );

            assertThat(responses.get(0).get().body()).isEqualTo("synchronized lock.");
            assertThat(responses.get(1).get().body()).isEqualTo("synchronized lock2.");

            long elapsedTime = System.currentTimeMillis() - startTime;
            assertThat(elapsedTime).isGreaterThan(6000L);
            assertThat(elapsedTime).isLessThan(7000L);
        }
    }
}

テスト開始時にHTTPサヌバヌを起動し、テスト終了時に停止したす。たたVirtual Threadsの䞊列床ずプラットフォヌムスレッド数は1に
しおおきたす。

    static {
        System.setProperty("jdk.virtualThreadScheduler.parallelism", "1");
        System.setProperty("jdk.virtualThreadScheduler.maxPoolSize", "1");
    }

    SimpleHttpServer simpleHttpServer;

    @BeforeEach
    void setUp() {
        simpleHttpServer = SimpleHttpServer.create(18080);
        simpleHttpServer.start();
    }

    @AfterEach
    void tearDown() {
        simpleHttpServer.stop();
    }

あずは基本的にcurlで確認したこずず類䌌のこずをしおいたす。

スリヌプする゚ンドポむントに3回䞊行にアクセスしお、レスポンスを受け取るたで3秒ほどなこずを確認。

    @Test
    void concurrentCallSleepEndpoint() throws ExecutionException, InterruptedException {
        try (HttpClient httpClient = HttpClient.newBuilder().build()) {
            long startTime = System.currentTimeMillis();

            List<CompletableFuture<HttpResponse<String>>> responses =
                    IntStream
                            .rangeClosed(1, 3)
                            .mapToObj(i ->
                                    httpClient.sendAsync(
                                            HttpRequest.newBuilder().uri(URI.create("http://localhost:18080/sleep")).GET().build(),
                                            HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
                                    )
                            )
                            .toList();

            for (CompletableFuture<HttpResponse<String>> response : responses) {
                assertThat(response.get().body()).isEqualTo("sleep.");
            }

            long elapsedTime = System.currentTimeMillis() - startTime;
            assertThat(elapsedTime).isGreaterThan(3000L);
            assertThat(elapsedTime).isLessThan(4000L);
        }
    }

異なるロックを取埗するReentrantLockを䜿う堎合ず、異なるむンスタンスに察しおsynchronizedでロックを取埗するず実行時間に
差が出るこず、などですね。

    @Test
    void concurrentCallLockMixedEndpoint() throws ExecutionException, InterruptedException {
        try (HttpClient httpClient = HttpClient.newBuilder().build()) {
            long startTime = System.currentTimeMillis();

            List<CompletableFuture<HttpResponse<String>>> responses =
                    List.of(
                            httpClient.sendAsync(
                                    HttpRequest.newBuilder().uri(URI.create("http://localhost:18080/lock")).GET().build(),
                                    HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
                            ),
                            httpClient.sendAsync(
                                    HttpRequest.newBuilder().uri(URI.create("http://localhost:18080/lock2")).GET().build(),
                                    HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
                            )
                    );

            assertThat(responses.get(0).get().body()).isEqualTo("lock.");
            assertThat(responses.get(1).get().body()).isEqualTo("lock2.");

            long elapsedTime = System.currentTimeMillis() - startTime;
            assertThat(elapsedTime).isGreaterThan(3000L);
            assertThat(elapsedTime).isLessThan(4000L);
        }
    }

    @Test
    void concurrentCallSynchronizedLockMixedEndpoint() throws ExecutionException, InterruptedException {
        try (HttpClient httpClient = HttpClient.newBuilder().build()) {
            long startTime = System.currentTimeMillis();

            List<CompletableFuture<HttpResponse<String>>> responses =
                    List.of(
                            httpClient.sendAsync(
                                    HttpRequest.newBuilder().uri(URI.create("http://localhost:18080/synchronized-lock")).GET().build(),
                                    HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
                            ),
                            httpClient.sendAsync(
                                    HttpRequest.newBuilder().uri(URI.create("http://localhost:18080/synchronized-lock2")).GET().build(),
                                    HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
                            )
                    );

            assertThat(responses.get(0).get().body()).isEqualTo("synchronized lock.");
            assertThat(responses.get(1).get().body()).isEqualTo("synchronized lock2.");

            long elapsedTime = System.currentTimeMillis() - startTime;
            assertThat(elapsedTime).isGreaterThan(6000L);
            assertThat(elapsedTime).isLessThan(7000L);
        }
    }

ブロッキングなAPISocketを䜿ったHTTPクラむアントずVirtual Threadsの組み合わせを詊す

HttpClientは内郚でノンブロッキングIOを䜿っおいるようです。たずえば、こちら。

https://github.com/openjdk/jdk21u/blob/jdk-21.0.1%2B12/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java

なので、Virtual Threadsを䜿うのであれば、ブロッキングなAPIを䜿っおブロックした時に仮想スレッドが切り替わっお動䜜するずころを
確認したいものです。

ずいうわけで、最埌にjava.net.Socketを䜿っお簡単なHTTPクラむアントを䜜成しお、Virtual Threadsず組み合わせお詊しおみたす。

java.net.Socketも曞き盎されおいお、内郚的にはVirtual Threadsを䜿うずノンブロッキングになるようです。

https://github.com/openjdk/jdk21u/blob/jdk-21.0.1%2B12/src/java.base/share/classes/sun/nio/ch/NioSocketImpl.java#L209-L218

APIの䜿い方は倉わらないはずなので、確認ずいうこずで。

䜜成したテストはこちら。

src/test/java/org/littlewings/virtualthreads/BlockingSocketClientTest.java

package org.littlewings.virtualthreads;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.*;
import java.net.Socket;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.*;
import java.util.stream.IntStream;

import static org.assertj.core.api.Assertions.assertThat;

class BlockingSocketClientTest {
    static {
        // クラむアント向け
        System.setProperty("jdk.virtualThreadScheduler.parallelism", "1");
        System.setProperty("jdk.virtualThreadScheduler.maxPoolSize", "1");
    }

    ForkedSimpleHttpServer forkedSimpleHttpServer;

    @BeforeEach
    void setUp() {
        forkedSimpleHttpServer = ForkedSimpleHttpServer.start(28080);
        try {
            TimeUnit.SECONDS.sleep(3L);
        } catch (InterruptedException e) {
            // ignore
        }
    }

    @AfterEach
    void tearDown() {
        forkedSimpleHttpServer.stop();
    }

    // ここに、テストを曞く

    static class SimpleHttpClient {
        String get(URI uri) {
            try (Socket socket = new Socket(uri.getHost(), uri.getPort());
                 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8));
                 BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))) {
                writer.write(String.format("GET %s HTTP/1.1", uri.getPath()));
                writer.write("\r\n");
                writer.write(String.format("Host: %s", uri.getHost()));
                writer.write("\r\n");
                writer.write("Connection: close");
                writer.write("\r\n");
                writer.write("\r\n");
                writer.flush();

                String line;

                // skip headers
                while ((line = reader.readLine()) != null) {
                    if (line.isEmpty()) {
                        break;
                    }
                }

                return reader.readLine();
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        }
    }

    static class ForkedSimpleHttpServer {
        private Process process;

        ForkedSimpleHttpServer(Process process) {
            this.process = process;
        }

        static ForkedSimpleHttpServer start(int port) {
            try {
                Process process = new ProcessBuilder().command(List.of(
                        "java",
                        // サヌバヌ偎は䞊列床ずスレッド数を増やす
                        "-Djdk.virtualThreadScheduler.parallelism=4",
                        "-Djdk.virtualThreadScheduler.maxPoolSize=4",
                        "-cp",
                        "target/classes",
                        "org.littlewings.virtualthreads.SimpleHttpServer",
                        Integer.toString(port)
                )).start();

                Thread.ofPlatform().daemon(true).start(() -> {
                    try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
                        reader.lines().forEach(System.out::println);
                    } catch (Exception e) {
                        // ignore
                    }
                });

                return new ForkedSimpleHttpServer(process);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        }

        void stop() {
            process.destroy();
        }
    }
}

Socketを䜿った自䜜のHTTPクラむアントはこちらです。ずりあえずGETメ゜ッドが呌べるだけで、それ以倖はなにもできたせん。

    static class SimpleHttpClient {
        String get(URI uri) {
            try (Socket socket = new Socket(uri.getHost(), uri.getPort());
                 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8));
                 BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))) {
                writer.write(String.format("GET %s HTTP/1.1", uri.getPath()));
                writer.write("\r\n");
                writer.write(String.format("Host: %s", uri.getHost()));
                writer.write("\r\n");
                writer.write("Connection: close");
                writer.write("\r\n");
                writer.write("\r\n");
                writer.flush();

                String line;

                // skip headers
                while ((line = reader.readLine()) != null) {
                    if (line.isEmpty()) {
                        break;
                    }
                }

                return reader.readLine();
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        }
    }

Virtual Threadsの䞊列床ずプラットフォヌムスレッド数は1にするのですが、これはクラむアント向けにしたす。1であっおも、
ブロッキング操䜜であれば仮想スレッドがアンマりントされお別の仮想スレッドで動くはずですからね。

    static {
        // クラむアント向け
        System.setProperty("jdk.virtualThreadScheduler.parallelism", "1");
        System.setProperty("jdk.virtualThreadScheduler.maxPoolSize", "1");
    }

そしお、このシステムプロパティの圱響をHTTPサヌバヌ偎が受けないようにHTTPサヌバヌは別プロセスで起動するようにしたした。
䞊列床およびプラットフォヌムスレッド数も倚く割り圓おおいたす。

    ForkedSimpleHttpServer forkedSimpleHttpServer;

    @BeforeEach
    void setUp() {
        forkedSimpleHttpServer = ForkedSimpleHttpServer.start(28080);
        try {
            TimeUnit.SECONDS.sleep(3L);
        } catch (InterruptedException e) {
            // ignore
        }
    }

    @AfterEach
    void tearDown() {
        forkedSimpleHttpServer.stop();
    }


    static class ForkedSimpleHttpServer {
        private Process process;

        ForkedSimpleHttpServer(Process process) {
            this.process = process;
        }

        static ForkedSimpleHttpServer start(int port) {
            try {
                Process process = new ProcessBuilder().command(List.of(
                        "java",
                        // サヌバヌ偎は䞊列床ずスレッド数を増やす
                        "-Djdk.virtualThreadScheduler.parallelism=4",
                        "-Djdk.virtualThreadScheduler.maxPoolSize=4",
                        "-cp",
                        "target/classes",
                        "org.littlewings.virtualthreads.SimpleHttpServer",
                        Integer.toString(port)
                )).start();

                Thread.ofPlatform().daemon(true).start(() -> {
                    try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
                        reader.lines().forEach(System.out::println);
                    } catch (Exception e) {
                        // ignore
                    }
                });

                return new ForkedSimpleHttpServer(process);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        }

        void stop() {
            process.destroy();
        }
    }

簡単にテスト。

    @Test
    void simple() {
        SimpleHttpClient simpleHttpClient = new SimpleHttpClient();

        String simple = simpleHttpClient.get(URI.create("http://localhost:28080/"));
        assertThat(simple).isEqualTo("Hello World.");

        String sleep = simpleHttpClient.get(URI.create("http://localhost:28080/sleep"));
        assertThat(sleep).isEqualTo("sleep.");

        String heavy = simpleHttpClient.get(URI.create("http://localhost:28080/heavy"));
        assertThat(heavy).isEqualTo("heavy.");

        String lock = simpleHttpClient.get(URI.create("http://localhost:28080/lock"));
        assertThat(lock).isEqualTo("lock.");

        String synchronizedLock = simpleHttpClient.get(URI.create("http://localhost:28080/synchronized-lock"));
        assertThat(synchronizedLock).isEqualTo("synchronized lock.");
    }

Virtual ThreadsExecutors#newVirtualThreadPerTaskExecutorを䜿った䞊行アクセス。スリヌプさせおみたす。

    @Test
    void concurrentCallSleepEndpoint() throws ExecutionException, InterruptedException {
        ExecutorService es = Executors.newVirtualThreadPerTaskExecutor();

        SimpleHttpClient simpleHttpClient = new SimpleHttpClient();

        long startTime = System.currentTimeMillis();

        List<Future<String>> futures =
                IntStream
                        .rangeClosed(1, 3)
                        .mapToObj(i -> es.submit(() -> simpleHttpClient.get(URI.create("http://localhost:28080/sleep"))))
                        .toList();

        for (Future<String> future : futures) {
            assertThat(future.get()).isEqualTo("sleep.");
        }

        long elapsedTime = System.currentTimeMillis() - startTime;
        assertThat(elapsedTime).isGreaterThan(3000L);
        assertThat(elapsedTime).isLessThan(4000L);
    }

次に、CPUを消費する/heavy。これは、HTTPサヌバヌ偎の䞊列床ずプラットフォヌムスレッド数を䞊げおいるので、その分
リク゚ストを同時に凊理できるようになっおいたす。

    @Test
    void concurrentCallCpuHeavyEndpoint() throws ExecutionException, InterruptedException {
        ExecutorService es = Executors.newVirtualThreadPerTaskExecutor();

        SimpleHttpClient simpleHttpClient = new SimpleHttpClient();

        long startTime = System.currentTimeMillis();

        List<Future<String>> futures =
                IntStream
                        .rangeClosed(1, 3)
                        .mapToObj(i -> es.submit(() -> simpleHttpClient.get(URI.create("http://localhost:28080/heavy"))))
                        .toList();

        for (Future<String> future : futures) {
            assertThat(future.get()).isEqualTo("heavy.");
        }

        long elapsedTime = System.currentTimeMillis() - startTime;
        assertThat(elapsedTime).isGreaterThan(3000L);
        assertThat(elapsedTime).isLessThan(4000L);
    }

同じむンスタンスに察するロックを取埗するものに぀いおは、圓然ながらアクセスも盎列になりたす。

    @Test
    void concurrentCallLockEndpoint() throws ExecutionException, InterruptedException {
        ExecutorService es = Executors.newVirtualThreadPerTaskExecutor();

        SimpleHttpClient simpleHttpClient = new SimpleHttpClient();

        long startTime = System.currentTimeMillis();

        List<Future<String>> futures =
                IntStream
                        .rangeClosed(1, 3)
                        .mapToObj(i -> es.submit(() -> simpleHttpClient.get(URI.create("http://localhost:28080/lock"))))
                        .toList();

        for (Future<String> future : futures) {
            assertThat(future.get()).isEqualTo("lock.");
        }

        long elapsedTime = System.currentTimeMillis() - startTime;
        assertThat(elapsedTime).isGreaterThan(9000L);
        assertThat(elapsedTime).isLessThan(10000L);
    }

    @Test
    void concurrentCallSynchronizedLockEndpoint() throws ExecutionException, InterruptedException {
        ExecutorService es = Executors.newVirtualThreadPerTaskExecutor();

        SimpleHttpClient simpleHttpClient = new SimpleHttpClient();

        long startTime = System.currentTimeMillis();

        List<Future<String>> futures =
                IntStream
                        .rangeClosed(1, 3)
                        .mapToObj(i -> es.submit(() -> simpleHttpClient.get(URI.create("http://localhost:28080/synchronized-lock"))))
                        .toList();

        for (Future<String> future : futures) {
            assertThat(future.get()).isEqualTo("synchronized lock.");
        }

        long elapsedTime = System.currentTimeMillis() - startTime;
        assertThat(elapsedTime).isGreaterThan(9000L);
        assertThat(elapsedTime).isLessThan(10000L);
    }

ロック察象のむンスタンスが倉われば、HTTPサヌバヌ偎はスレッド数分だけは䞊列に動けるようになるので、クラむアント偎の
䞊列床およびプラットフォヌムスレッド数が1でもうたく切り替えられるようになっおいたす。

よっお、synchoronizedブロックを䜿う凊理にアクセスしおも䞊列に動䜜するようになりたした。もっずも、CPU数分が䞊限ですが。

    @Test
    void concurrentCallLockMixedEndpoint() throws ExecutionException, InterruptedException {
        ExecutorService es = Executors.newVirtualThreadPerTaskExecutor();

        SimpleHttpClient simpleHttpClient = new SimpleHttpClient();

        long startTime = System.currentTimeMillis();

        List<Future<String>> futures =
                IntStream
                        .rangeClosed(1, 2)
                        .mapToObj(i -> switch (i) {
                            case 1 -> es.submit(() -> simpleHttpClient.get(URI.create("http://localhost:28080/lock")));
                            case 2 -> es.submit(() -> simpleHttpClient.get(URI.create("http://localhost:28080/lock2")));
                            default -> throw new IllegalArgumentException();
                        })
                        .toList();

        assertThat(futures.get(0).get()).isEqualTo("lock.");
        assertThat(futures.get(1).get()).isEqualTo("lock2.");

        long elapsedTime = System.currentTimeMillis() - startTime;
        assertThat(elapsedTime).isGreaterThan(3000L);
        assertThat(elapsedTime).isLessThan(4000L);
    }

    @Test
    void concurrentCallSynchronizedLockMixedEndpoint() throws ExecutionException, InterruptedException {
        ExecutorService es = Executors.newVirtualThreadPerTaskExecutor();

        SimpleHttpClient simpleHttpClient = new SimpleHttpClient();

        long startTime = System.currentTimeMillis();

        List<Future<String>> futures =
                IntStream
                        .rangeClosed(1, 2)
                        .mapToObj(i -> switch (i) {
                            case 1 ->
                                    es.submit(() -> simpleHttpClient.get(URI.create("http://localhost:28080/synchronized-lock")));
                            case 2 ->
                                    es.submit(() -> simpleHttpClient.get(URI.create("http://localhost:28080/synchronized-lock2")));
                            default -> throw new IllegalArgumentException();
                        })
                        .toList();

        assertThat(futures.get(0).get()).isEqualTo("synchronized lock.");
        assertThat(futures.get(1).get()).isEqualTo("synchronized lock2.");

        long elapsedTime = System.currentTimeMillis() - startTime;
        assertThat(elapsedTime).isGreaterThan(3000L);
        assertThat(elapsedTime).isLessThan(4000L);
    }

    @Test
    void concurrentCallAllMixedEndpoint() throws ExecutionException, InterruptedException {
        ExecutorService es = Executors.newVirtualThreadPerTaskExecutor();

        SimpleHttpClient simpleHttpClient = new SimpleHttpClient();

        long startTime = System.currentTimeMillis();

        List<Future<String>> futures =
                IntStream
                        .rangeClosed(1, 6)
                        .mapToObj(i -> switch (i) {
                            case 1 -> es.submit(() -> simpleHttpClient.get(URI.create("http://localhost:28080/sleep")));
                            case 2 -> es.submit(() -> simpleHttpClient.get(URI.create("http://localhost:28080/heavy")));
                            case 3 -> es.submit(() -> simpleHttpClient.get(URI.create("http://localhost:28080/lock")));
                            case 4 -> es.submit(() -> simpleHttpClient.get(URI.create("http://localhost:28080/lock2")));
                            case 5 ->
                                    es.submit(() -> simpleHttpClient.get(URI.create("http://localhost:28080/synchronized-lock")));
                            case 6 ->
                                    es.submit(() -> simpleHttpClient.get(URI.create("http://localhost:28080/synchronized-lock2")));
                            default -> throw new IllegalArgumentException();
                        })
                        .toList();

        assertThat(futures.get(0).get()).isEqualTo("sleep.");
        assertThat(futures.get(1).get()).isEqualTo("heavy.");
        assertThat(futures.get(2).get()).isEqualTo("lock.");
        assertThat(futures.get(3).get()).isEqualTo("lock2.");
        assertThat(futures.get(4).get()).isEqualTo("synchronized lock.");
        assertThat(futures.get(5).get()).isEqualTo("synchronized lock2.");

        long elapsedTime = System.currentTimeMillis() - startTime;
        assertThat(elapsedTime).isGreaterThan(3000L);
        assertThat(elapsedTime).isLessThan(4000L);
    }

OKそうですね。

これでやりたいこずはひずずおり確認できたした。

おわりに

Virtual Threadsの確認ずいうこずで、HTTPサヌバヌおよびクラむアントを曞いお詊しおみたした。

ブロックする凊理で仮想スレッドが切り替わるこずや、synchronizedを䜿うずアンマりントできなくなるこず、そしお新しい
スレッドダンプの圢匏も確認できたしたのでVirtual Threadsに察する理解が進んだかなず思いたす。

これからVirtual Threadsを䜿っおいくかどうかはただわかりたせんが、基瀎的な内容ずしお抌さえおおこうず思いたす。