CLOVER🍀

That was when it all began.

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

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

前に、Virtual Threadsに関する゚ントリヌを曞きたした。

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

Virtual Threadsを使ってHTTPサーバー/クライアントを書いて、スレッドまわりの動きを確認してみる(スレッドダンプの取得付き) - CLOVER🍀

この䞭で、pinningピン留めに぀いおの確認を少し忘れおいたので続きをやろうず思いたす。

Virtual Threadsのpinningピン留め

JEP 444Virtual Threadsに぀いおはこちら。

JEP 444: Virtual Threads

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

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

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

pinningピン留めは、プラットフォヌムスレッドから仮想スレッドをアンマりントできない状態のこずを蚀いたす。
以䞋のケヌスがあるようです。

  • When it executes code inside a synchronized block or methodsynchronizedブロックやsynchronizedメ゜ッド内のコヌドを実行しおいる時
  • When it executes a native method or a foreign functionネむティブメ゜ッドやJEP 424のForeign Functionを実行しおいる時

pinningピン留めが発生しおもアプリケヌションがおかしくなるこずはありたせんが、スケヌラビリティが代償になる可胜性がありたす。  数に限りのあるプラットフォヌムスレッドがひず぀の仮想スレッドに瞛られお、他の仮想スレッドを動かせず数が枛っおしたうからですね。

これを回避するには、synchronizedブロックおよびsynchronizedメ゜ッドを䜿わなくお枈むようにするこずはもちろんですが、
ロックが必芁な堎合はReentrantLockを䜿うように修正するこずです。

そしお、synchronizedブロックでアンマりントできなくなるこずは以䞋の゚ントリヌで確認しおいたす。

Virtual Threadsを使ってHTTPサーバー/クライアントを書いて、スレッドまわりの動きを確認してみる(スレッドダンプの取得付き) - CLOVER🍀

ReentrantLockの利甚であれば問題ないこずも確認しおいたす。

pinningピン留めが発生したこずは、以䞋の方法で蚘録できるようです。

  • A JDK Flight Recorder (JFR) event is emitted when a thread blocks while pinned (see JDK Flight Recorder).
  • The system property jdk.tracePinnedThreads triggers a stack trace when a thread blocks while pinned. Running with -Djdk.tracePinnedThreads=full prints a complete stack trace when a thread blocks while pinned, highlighting native frames and frames holding monitors. Running with -Djdk.tracePinnedThreads=short limits the output to just the problematic frames.

今回は、jdk.tracePinnedThreadsシステムプロパティを詊しおみたいず思いたす。これは、pinningピン留めが発生した時に
スタックトレヌスを出力するものです。

指定方法には2皮類あるようです。

  • -Djdk.tracePinnedThreads=full 
 pinningピン留めが発生した時に、完党なスタックトレヌスずネむティブフレヌムおよびモニタヌを保持しおいるフレヌムが匷調衚瀺される
  • -Djdk.tracePinnedThreads=short 
 問題のあるフレヌムのみが出力される

こちらのドキュメントにも蚘茉がありたす。

コア・ラむブラリ / 䞊行凊理 / 仮想スレッド / 仮想スレッド: 採甚ガむド / 長時間か぀頻繁な固定の回避

動䜜確認には、こちらで曞いたHTTPサヌバヌを䜿うこずにしたす。

Virtual Threadsを使ってHTTPサーバー/クライアントを書いて、スレッドまわりの動きを確認してみる(スレッドダンプの取得付き) - CLOVER🍀

では、詊しおみたしょう。

環境

今回の環境はこちら。

$ java --version
openjdk 21.0.2 2024-01-16
OpenJDK Runtime Environment (build 21.0.2+13-Ubuntu-122.04.1)
OpenJDK 64-Bit Server VM (build 21.0.2+13-Ubuntu-122.04.1, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 21.0.2, 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-101-generic", arch: "amd64", family: "unix"

準備

Mavenの蚭定ですが、特に䟝存関係などは䜿いたせん。

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

゜ヌスコヌドはこちら。

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", "")));
            log(String.format("jdk.tracePinnedThreads = %s", System.getProperty("jdk.tracePinnedThreads", "")));

            // 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()));
    }
}

基本的に、もずの゚ントリヌず内容はほが倉えおいたせん。JDKのHttpServerを䜿っお、Virtual Threadsを䜿った簡単なHTTPサヌバヌを
曞いおいたす。

    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", "")));
            log(String.format("jdk.tracePinnedThreads = %s", System.getProperty("jdk.tracePinnedThreads", "")));

            // 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);
        }
    }

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

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

アクセスパスごずの凊理は、caseになっおいたす。

この䞭でpinningピン留めが発生しそうなのは、/synchronized-lock、/synchronized-lock2かなず思いたす。/heavyも仮想スレッドが
束瞛され続けたすが、どうなるんでしょうね

では、動かしおみたしょう。

確認しおみる

たずはコンパむル。

$ mvn compile

起動は、以䞋のコマンドで行いたす。

$ java \
    -Djdk.virtualThreadScheduler.parallelism=1 \
    -Djdk.virtualThreadScheduler.maxPoolSize=1 \
    -Djdk.tracePinnedThreads=[full or short] \
    -cp target/classes \
    org.littlewings.virtualthreads.SimpleHttpServer

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

-Djdk.tracePinnedThreads=fullず-Djdk.tracePinnedThreads=shortでそれぞれ確認しおみたしょう。

-Djdk.tracePinnedThreads=full

最初は、-Djdk.tracePinnedThreads=fullで確認しおみたしょう。

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

各アクセスパスごずに確認しおみたす。

/sleep

TimeUnit#sleepでスリヌプする、/sleepにアクセス。2぀のリク゚ストは、別々のタヌミナルで連続で実行しおいたす。

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


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

HTTPサヌバヌ偎のログ䞊は、倉わったずころはありたせん。

[2024-04-03 23:09:24] - handler-1 - access[GET:/sleep] start
[2024-04-03 23:09:24] - handler-2 - access[GET:/sleep] start
[2024-04-03 23:09:27] - handler-1 - access[GET:/sleep] end
[2024-04-03 23:09:27] - handler-2 - access[GET:/sleep] end
/heavy

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

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


## 2぀目
$ time curl localhost:8080/heavy
heavy.
real    0m5.642s
user    0m0.004s
sys     0m0.003s

こちらのログも、倉わったずころはありたせんでした。

[2024-04-03 23:13:26] - handler-5 - access[GET:/heavy] start
[2024-04-03 23:13:29] - handler-5 - access[GET:/heavy] end
[2024-04-03 23:13:29] - handler-6 - access[GET:/heavy] start
[2024-04-03 23:13:32] - handler-6 - access[GET:/heavy] end

/lock

ReentrantLockを䜿う/lock。

## ひず぀目
$ time curl localhost:8080/lock
lock.
real    0m3.012s
user    0m0.001s
sys     0m0.009s


## 2぀目
$ time curl localhost:8080/lock
lock.
real    0m5.662s
user    0m0.006s
sys     0m0.005s

ログ。

[2024-04-03 23:26:15] - handler-9 - access[GET:/lock] start
[2024-04-03 23:26:16] - handler-10 - access[GET:/lock] start
[2024-04-03 23:26:18] - handler-9 - access[GET:/lock] end
[2024-04-03 23:26:21] - handler-10 - access[GET:/lock] end

ReentrantLockを䜿っおいるので問題ないですね。/lock2はもう飛ばすこずにしたす。

/synchronized-lock

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

## ひず぀目
$ time curl localhost:8080/synchronized-lock
synchronized lock.
real    0m3.013s
user    0m0.008s
sys     0m0.003s


## 2぀目
$ time curl localhost:8080/synchronized-lock
synchronized lock.
real    0m5.543s
user    0m0.001s
sys     0m0.009s

ログはこうなりたした。

[2024-04-03 23:27:36] - handler-13 - access[GET:/synchronized-lock] start
Thread[#39,ForkJoinPool-1-worker-2,5,CarrierThreads]
    java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:183)
    java.base/jdk.internal.vm.Continuation.onPinned0(Continuation.java:393)
    java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:621)
    java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:791)
    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:160) <== monitors:1
    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)
[2024-04-03 23:27:39] - handler-13 - access[GET:/synchronized-lock] end
[2024-04-03 23:27:39] - handler-14 - access[GET:/synchronized-lock] start
[2024-04-03 23:27:42] - handler-14 - access[GET:/synchronized-lock] end

なるほど、確かにスタックトレヌスが出力されおいたす。しかも、どこでsynchronizedに入っおいるかがわかるんですねmonitors。

䞀定時間が経過しおから出力される、ずいうより即出力されるようですが、連続で実行した時の2回目の方はスタックトレヌスが出力されお
いないんですよね。さらに远加で実行しおも出力されたせんでした。これはどうしおなのでしょう

この理由は、あずで調べおみたした。

/synchronized-lock2も飛ばすこずにしたす。

-Djdk.tracePinnedThreads=short

次はHTTPサヌバヌを再起動しお、-Djdk.tracePinnedThreads=shortで確認しおみたしょう。

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

/synchronized-lock

もう結果がわかっおいるので、synchronizedでロックを取る/synchronized-lockのみで詊したす。

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


## 2぀目
$ time curl localhost:8080/synchronized-lock
synchronized lock.
real    0m5.700s
user    0m0.008s
sys     0m0.003s

ログはこのようになりたした。スタックトレヌスが非垞にシンプルずいうか、問題になった箇所のみが出力されおいたす。

[2024-04-03 23:31:08] - handler-1 - access[GET:/synchronized-lock] start
Thread[#25,ForkJoinPool-1-worker-1,5,CarrierThreads]
    org.littlewings.virtualthreads.SimpleHttpServer.lambda$createHandler$1(SimpleHttpServer.java:160) <== monitors:1
[2024-04-03 23:31:11] - handler-1 - access[GET:/synchronized-lock] end
[2024-04-03 23:31:11] - handler-2 - access[GET:/synchronized-lock] start
[2024-04-03 23:31:14] - handler-2 - access[GET:/synchronized-lock] end

確かにshortです。

これで、システムプロパティjdk.tracePinnedThreadsの効果がわかりたしたね。

もう少し

ここからは、少し実装を芋おみたしょう。

システムプロパティjdk.tracePinnedThreadsの参照箇所

こちらで芋おいるようです。

    private static int tracePinningMode() {
        String propValue = GetPropertyAction.privilegedGetProperty("jdk.tracePinnedThreads");
        if (propValue != null) {
            if (propValue.length() == 0 || "full".equalsIgnoreCase(propValue))
                return 1;
            if ("short".equalsIgnoreCase(propValue))
                return 2;
        }
        return 0;
    }

https://github.com/openjdk/jdk21u/blob/jdk-21.0.2%2B13/src/java.base/share/classes/java/lang/VirtualThread.java#L1160-L1169

これを芋るず、システムプロパティjdk.tracePinnedThreadsのみを指定した堎合はfullを指定した堎合ず同矩になるようですね。

この結果は定数ずしお保持され

    private static final int TRACE_PINNING_MODE = tracePinningMode();

https://github.com/openjdk/jdk21u/blob/jdk-21.0.2%2B13/src/java.base/share/classes/java/lang/VirtualThread.java#L73

VThreadContinuation#onPinnedでpinningピン留めを怜出した時に、システムプロパティjdk.tracePinnedThreadsが指定されおいれば
即座に出力されたす。

        @Override
        protected void onPinned(Continuation.Pinned reason) {
            if (TRACE_PINNING_MODE > 0) {
                boolean printAll = (TRACE_PINNING_MODE == 1);
                PinnedThreadPrinter.printStackTrace(System.out, printAll);
            }
        }

https://github.com/openjdk/jdk21u/blob/jdk-21.0.2%2B13/src/java.base/share/classes/java/lang/VirtualThread.java#L179-L185

TRACE_PINNING_MODEが1の堎合はfullですね。

pinningピン留めの状態かどうかは、こちらで怜出したす。

    @Hidden
    private boolean yield0(ContinuationScope scope, Continuation child) {
        preempted = false;

        if (scope != this.scope)
            this.yieldInfo = scope;
        int res = doYield();
        U.storeFence(); // needed to prevent certain transformations by the compiler

        assert scope != this.scope || yieldInfo == null : "scope: " + scope + " this.scope: " + this.scope + " yieldInfo: " + yieldInfo + " res: " + res;
        assert yieldInfo == null || scope == this.scope || yieldInfo instanceof Integer : "scope: " + scope + " this.scope: " + this.scope + " yieldInfo: " + yieldInfo + " res: " + res;

        if (child != null) { // TODO: ugly
            if (res != 0) {
                child.yieldInfo = res;
            } else if (yieldInfo != null) {
                assert yieldInfo instanceof Integer;
                child.yieldInfo = yieldInfo;
            } else {
                child.yieldInfo = res;
            }
            this.yieldInfo = null;
        } else {
            if (res == 0 && yieldInfo != null) {
                res = (Integer)yieldInfo;
            }
            this.yieldInfo = null;

            if (res == 0)
                onContinue();
            else
                onPinned0(res);
        }
        assert yieldInfo == null;

        return res == 0;
    }

https://github.com/openjdk/jdk21u/blob/jdk-21.0.2%2B13/src/java.base/share/classes/jdk/internal/vm/Continuation.java#L354-L390

Continuation#doYieldメ゜ッドはnativeメ゜ッドなので詳现は芋おいたせん。

        int res = doYield();

そしお、この結果が0でない堎合はpinningピン留めず刀定したす。

            if (res == 0)
                onContinue();
            else
                onPinned0(res);

ここからVThreadContinuation#onPinnedを呌び出すわけですね。

    private void onPinned0(int reason) {
        onPinned(pinnedReason(reason));
    }

https://github.com/openjdk/jdk21u/blob/jdk-21.0.2%2B13/src/java.base/share/classes/jdk/internal/vm/Continuation.java#L392-L394

Coninuation#pinnedReasonの内容はこちら。

    private static Pinned pinnedReason(int reason) {
        return switch (reason) {
            case 2 -> Pinned.CRITICAL_SECTION;
            case 3 -> Pinned.NATIVE;
            case 4 -> Pinned.MONITOR;
            default -> throw new AssertionError("Unknown pinned reason: " + reason);
        };
    }

https://github.com/openjdk/jdk21u/blob/jdk-21.0.2%2B13/src/java.base/share/classes/jdk/internal/vm/Continuation.java#L82-L89

Pinnedずいうのは列挙型で、pinningピン留めの理由を衚したす。

    /** Reason for pinning */
    public enum Pinned {
        /** Native frame on stack */ NATIVE,
        /** Monitor held */          MONITOR,
        /** In critical section */   CRITICAL_SECTION }

https://github.com/openjdk/jdk21u/blob/jdk-21.0.2%2B13/src/java.base/share/classes/jdk/internal/vm/Continuation.java#L57-L61

あずは先ほどのシステムプロパティjdk.tracePinnedThreadsが指定されおいる時に、VThreadContinuation#onPinnedが呌び出しおいる
PinnedThreadPrinter#printStackTraceで、スタックトレヌスを出力するずいうこずになりたす。

    /**
     * Prints the continuation stack trace.
     *
     * @param printAll true to print all stack frames, false to only print the
     *        frames that are native or holding a monitor
     */
    static void printStackTrace(PrintStream out, boolean printAll) {
        List<LiveStackFrame> stack = STACK_WALKER.walk(s ->
            s.map(f -> (LiveStackFrame) f)
                    .filter(f -> f.getDeclaringClass() != PinnedThreadPrinter.class)
                    .collect(Collectors.toList())
        );

        // find the closest frame that is causing the thread to be pinned
        stack.stream()
            .filter(f -> (f.isNativeMethod() || f.getMonitors().length > 0))
            .map(LiveStackFrame::getDeclaringClass)
            .findFirst()
            .ifPresentOrElse(klass -> {
                int hash = hash(stack);
                Hashes hashes = HASHES.get(klass);
                synchronized (hashes) {
                    // print the stack trace if not already seen
                    if (hashes.add(hash)) {
                        printStackTrace(stack, out, printAll);
                    }
                }
            }, () -> printStackTrace(stack, out, true));  // not found
    }
スタックトレヌスが1床しか出力されなかったのは

先ほど蚘茉した箇所に答えが茉っおいるのですが、ここですね。

                int hash = hash(stack);
                Hashes hashes = HASHES.get(klass);
                synchronized (hashes) {
                    // print the stack trace if not already seen
                    if (hashes.add(hash)) {
                        printStackTrace(stack, out, printAll);
                    }
                }

https://github.com/openjdk/jdk21u/blob/jdk-21.0.2%2B13/src/java.base/share/classes/java/lang/PinnedThreadPrinter.java#L107-L114

スタックフレヌムのハッシュ倀を取り、同じスタックフレヌムからは2床出力しないようになっおいたす。

    /**
     * Returns a hash of the given stack trace. The hash is based on the class,
     * method and bytecode index.
     */
    private static int hash(List<LiveStackFrame> stack) {
        int hash = 0;
        for (LiveStackFrame frame : stack) {
            hash = (31 * hash) + Objects.hash(frame.getDeclaringClass(),
                    frame.getMethodName(),
                    frame.getByteCodeIndex());
        }
        return hash;
    }

https://github.com/openjdk/jdk21u/blob/jdk-21.0.2%2B13/src/java.base/share/classes/java/lang/PinnedThreadPrinter.java#L74-L86

synchronizedブロックを実行するパスに2回アクセスしたのに、1床しか出力されなかったのはこれが理由ですね。

おわりに

JEP 444Virtual Threadsのpinningピン留めが起こった際に、システムプロパティjdk.tracePinnedThreadsによる
スタックトレヌスが出力されるこずを確認しおみたした。

pinningピン留め自䜓に぀いおは前回も確認しおいたしたが、この機胜のこずを忘れおいたので今回確認しおおいおよかったです。
pinningピン留めずいうものがどういうものか、どういう条件でスタックトレヌスを出力させるこずができるかも確認できたしたしね。