サンプルコードで身につける: 1リクエスト1スレッドと仮想スレッド
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1ハンドラを動かしているスレッドの実物
の中で Thread.currentThread() を文字列にして返した結果です(##### の行はサーバに渡した起動引数)。既定では http-nio-8080-exec-1 という ので、virtual=false でした。=true で起動すると tomcat-handler-0 というに替わります。
##### server args:toString=Thread[#41,http-nio-8080-exec-1,5,main] name=http-nio-8080-exec-1 virtual=false...##### server args: --spring.threads.virtual.enabled=truetoString=VirtualThread[#46,tomcat-handler-0]/runnable@ForkJoinPool-1-worker-1 name=tomcat-handler-0 virtual=true
2素の Java で: 20 本の固定プールと仮想スレッド
も Tomcat も使わず、200 ミリ秒眠るだけのを 100 本流します。 20 本のでは 20 本ずつしか進まないので5巡ぶんかかり、タスクごとに仮想スレッドを1本使う方は眠っている間に次のタスクが始まります。所要時間は実行ごとに変わるため、大小関係だけを出力しています。
31秒眠るハンドラへ同時 400 件を5回ずつ
wall_ms が全体の完了時間、over1500ms が 1.5 秒を超えたリクエストの数で、既定(上限 200)では5回とも 200 件ちょうどが後回しになりました。仮想スレッドと、上限を 400 に上げた設定では、約 1.1〜1.2 秒の回と約 2.1〜2.2 秒の回が混じっています。接続待ち行列の長さ()を 1000 にした別の5回では仮想スレッドの外れは 0 回でしたが、原因までは確かめていません。
##### server args:-- N=400 run 1requests=400 concurrency=400 ok=400 failed=0 wall_ms=2209 min_ms=1145 p50_ms=2102 max_ms=2154 over1500ms=200-- N=400 run 2requests=400 concurrency=400 ok=400 failed=0 wall_ms=2089 min_ms=1037 p50_ms=1986 max_ms=2033 over1500ms=200-- N=400 run 3requests=400 concurrency=400 ok=400 failed=0 wall_ms=2096 min_ms=1037 p50_ms=1985 max_ms=2046 over1500ms=200-- N=400 run 4requests=400 concurrency=400 ok=400 failed=0 wall_ms=2088 min_ms=1052 p50_ms=2004 max_ms=2050 over1500ms=200-- N=400 run 5requests=400 concurrency=400 ok=400 failed=0 wall_ms=2090 min_ms=1042 p50_ms=2011 max_ms=2078 over1500ms=200##### server args: --spring.threads.virtual.enabled=true-- N=400 run 1requests=400 concurrency=400 ok=400 failed=0 wall_ms=2185 min_ms=1091 p50_ms=1154 max_ms=2115 over1500ms=57-- N=400 run 2requests=400 concurrency=400 ok=400 failed=0 wall_ms=1120 min_ms=1023 p50_ms=1062 max_ms=1115 over1500ms=0-- N=400 run 3requests=400 concurrency=400 ok=400 failed=0 wall_ms=1117 min_ms=1022 p50_ms=1057 max_ms=1107 over1500ms=0-- N=400 run 4requests=400 concurrency=400 ok=400 failed=0 wall_ms=2139 min_ms=1044 p50_ms=1071 max_ms=2103 over1500ms=14-- N=400 run 5requests=400 concurrency=400 ok=400 failed=0 wall_ms=1117 min_ms=1001 p50_ms=1027 max_ms=1079 over1500ms=0##### server args: --server.tomcat.threads.max=400-- N=400 run 1requests=400 concurrency=400 ok=400 failed=0 wall_ms=2143 min_ms=1127 p50_ms=1182 max_ms=2059 over1500ms=15-- N=400 run 2requests=400 concurrency=400 ok=400 failed=0 wall_ms=2110 min_ms=1019 p50_ms=1055 max_ms=2079 over1500ms=6-- N=400 run 3requests=400 concurrency=400 ok=400 failed=0 wall_ms=1129 min_ms=1035 p50_ms=1073 max_ms=1128 over1500ms=0-- N=400 run 4requests=400 concurrency=400 ok=400 failed=0 wall_ms=2127 min_ms=1018 p50_ms=1055 max_ms=2099 over1500ms=39-- N=400 run 5requests=400 concurrency=400 ok=400 failed=0 wall_ms=1105 min_ms=1019 p50_ms=1042 max_ms=1089 over1500ms=0
4対比: Node 24 で待つ処理と回り続ける処理
同じ負荷ツールを Node 24(v24.21.0)のサーバへ向けた結果です。await で待つ /sleep は 400 件でも約 1.1 秒でしたが、時計が1秒進むまでループする /block は 20 件で約 20 秒かかりました。最小が約1秒、最大が約 20 秒で、JavaScript を動かす1本のスレッドを /block が手放さないため、1件ずつ順番に進んだ形です。
listening on 8080...-- /sleep N=400 run 1requests=400 concurrency=400 ok=400 failed=0 wall_ms=1110 min_ms=1019 p50_ms=1047 max_ms=1094 over1500ms=0-- /sleep N=400 run 2requests=400 concurrency=400 ok=400 failed=0 wall_ms=1131 min_ms=1031 p50_ms=1071 max_ms=1125 over1500ms=0-- /sleep N=400 run 3requests=400 concurrency=400 ok=400 failed=0 wall_ms=1104 min_ms=1017 p50_ms=1044 max_ms=1084 over1500ms=0-- /block N=20 run 1requests=20 concurrency=20 ok=20 failed=0 wall_ms=20028 min_ms=1030 p50_ms=11025 max_ms=20026 over1500ms=19-- /block N=20 run 2requests=20 concurrency=20 ok=20 failed=0 wall_ms=20031 min_ms=1031 p50_ms=11026 max_ms=20029 over1500ms=19
// server.mjs(全文。コメントは日本語に訳してある)
import http from 'node:http';
import os from 'node:os';
import { setTimeout as sleep } from 'node:timers/promises';
const server = http.createServer(async (req, res) => {
if (req.url === '/sleep') {
await sleep(1000); // 待つあいだ、イベントループに処理を返す
res.end('await-sleep');
} else if (req.url === '/block') {
const end = Date.now() + 1000;
while (Date.now() < end) {} // 時計が1秒進むまで回る。1本しかないスレッドを握ったまま
res.end('busy-wait');
} else {
res.end(`node ${process.version} cpus=${os.availableParallelism()}`);
}
});
server.listen(8080, () => console.log('listening on 8080'));
5回り続ける処理を Spring に投げると
while (System.currentTimeMillis() < end) {} で1秒回り続けるハンドラ /block への結果です。既定のスレッドでは 40 件でも約 1.2 秒で、仮想スレッドでは 20 件で約2秒、40 件で約4秒でした(1.5 秒を超えた件数からは、この環境の CPU 数と同じ 10 件ずつ進んだように見えます)。ただし各スレッドは時計を見て終わっただけなので、40 件ぶんの計算を 1.2 秒でこなしたという意味ではありません。
##### server args:...-- /block N=20 run 1requests=20 concurrency=20 ok=20 failed=0 wall_ms=1053 min_ms=1040 p50_ms=1044 max_ms=1049 over1500ms=0-- /block N=20 run 2requests=20 concurrency=20 ok=20 failed=0 wall_ms=1093 min_ms=1042 p50_ms=1051 max_ms=1092 over1500ms=0-- /block N=40 run 1requests=40 concurrency=40 ok=40 failed=0 wall_ms=1248 min_ms=1125 p50_ms=1134 max_ms=1243 over1500ms=0-- /block N=40 run 2requests=40 concurrency=40 ok=40 failed=0 wall_ms=1198 min_ms=1061 p50_ms=1099 max_ms=1191 over1500ms=0##### server args: --spring.threads.virtual.enabled=true...-- /block N=20 run 1requests=20 concurrency=20 ok=20 failed=0 wall_ms=2063 min_ms=1060 p50_ms=2030 max_ms=2061 over1500ms=10-- /block N=20 run 2requests=20 concurrency=20 ok=20 failed=0 wall_ms=2044 min_ms=1041 p50_ms=2027 max_ms=2040 over1500ms=10-- /block N=40 run 1requests=40 concurrency=40 ok=40 failed=0 wall_ms=4052 min_ms=1044 p50_ms=3036 max_ms=4045 over1500ms=30-- /block N=40 run 2requests=40 concurrency=40 ok=40 failed=0 wall_ms=4039 min_ms=1038 p50_ms=3026 max_ms=4037 over1500ms=30