[{"data":1,"prerenderedAt":1729},["ShallowReactive",2],{"blog-\u002Fblog\u002Fjava-virtual-threads":3},{"id":4,"title":5,"body":6,"description":1715,"difficulty":1716,"extension":1717,"framework":1718,"frameworkSlug":135,"meta":1719,"navigation":183,"order":336,"path":1720,"qaPath":1721,"seo":1722,"stem":1723,"subtopic":1724,"topic":1725,"topicSlug":1726,"updated":1727,"__hash__":1728},"blog\u002Fblog\u002Fjava-virtual-threads.md","Java Virtual Threads (Project Loom) — Thread-per-Request at Scale",{"type":7,"value":8,"toc":1696},"minimark",[9,14,18,26,37,41,130,219,223,230,263,271,274,278,285,479,486,490,652,655,659,665,668,677,767,771,774,779,801,814,818,827,833,989,994,998,1003,1151,1160,1163,1167,1170,1183,1193,1199,1292,1296,1299,1472,1475,1496,1500,1589,1596,1600,1657,1661,1692],[10,11,13],"h2",{"id":12},"the-concurrency-problem-virtual-threads-solve","The concurrency problem virtual threads solve",[15,16,17],"p",{},"Traditional Java concurrency has a fundamental tension: OS threads are expensive (~1 MB\nstack, ~ms to create, OS context-switch overhead), so frameworks limit thread counts with\nthread pools. But blocking I\u002FO (database queries, HTTP calls, file reads) holds a thread\nidle while waiting. The result: a server handling 10,000 concurrent requests with a pool\nof 200 threads spends most of its time context-switching and queuing, not doing work.",[15,19,20,21,25],{},"The workarounds — reactive programming, ",[22,23,24],"code",{},"CompletableFuture"," chains, async\u002Fawait — work\nbut transform simple sequential code into complex callback trees that are hard to read,\ndebug, and reason about.",[15,27,28,32,33,36],{},[29,30,31],"strong",{},"Virtual threads"," (finalized in ",[29,34,35],{},"Java 21",", JEP 444, Project Loom) solve this at the\nJVM level: you write blocking, sequential code, and the JVM manages the scheduling so\nthat OS threads are never held idle.",[10,38,40],{"id":39},"platform-threads-vs-virtual-threads","Platform threads vs virtual threads",[42,43,44,60],"table",{},[45,46,47],"thead",{},[48,49,50,54,57],"tr",{},[51,52,53],"th",{},"Aspect",[51,55,56],{},"Platform Thread",[51,58,59],{},"Virtual Thread",[61,62,63,75,86,97,108,119],"tbody",{},[48,64,65,69,72],{},[66,67,68],"td",{},"OS mapping",[66,70,71],{},"1:1 with OS thread",[66,73,74],{},"Many virtual → few OS (M:N)",[48,76,77,80,83],{},[66,78,79],{},"Stack memory",[66,81,82],{},"~1 MB (fixed)",[66,84,85],{},"Few KB (heap-allocated, growable)",[48,87,88,91,94],{},[66,89,90],{},"Creation time",[66,92,93],{},"~1 ms",[66,95,96],{},"~1 µs",[48,98,99,102,105],{},[66,100,101],{},"Max concurrent",[66,103,104],{},"~thousands",[66,106,107],{},"~millions",[48,109,110,113,116],{},[66,111,112],{},"Blocking I\u002FO",[66,114,115],{},"Blocks the OS thread",[66,117,118],{},"JVM unmounts; OS thread freed",[48,120,121,124,127],{},[66,122,123],{},"Thread pool needed",[66,125,126],{},"Yes (to limit cost)",[66,128,129],{},"Generally no",[131,132,137],"pre",{"className":133,"code":134,"language":135,"meta":136,"style":136},"language-java shiki shiki-themes github-light github-dark","\u002F\u002F Platform thread — OS thread behind the scenes:\nThread platform = new Thread(() -> doWork());\n\n\u002F\u002F Virtual thread — JVM-managed, cheap:\nThread virtual = Thread.ofVirtual().start(() -> doWork());\n","java","",[22,138,139,148,178,185,191],{"__ignoreMap":136},[140,141,144],"span",{"class":142,"line":143},"line",1,[140,145,147],{"class":146},"sJ8bj","\u002F\u002F Platform thread — OS thread behind the scenes:\n",[140,149,151,155,159,162,166,169,172,175],{"class":142,"line":150},2,[140,152,154],{"class":153},"sVt8B","Thread platform ",[140,156,158],{"class":157},"szBVR","=",[140,160,161],{"class":157}," new",[140,163,165],{"class":164},"sScJk"," Thread",[140,167,168],{"class":153},"(() ",[140,170,171],{"class":157},"->",[140,173,174],{"class":164}," doWork",[140,176,177],{"class":153},"());\n",[140,179,181],{"class":142,"line":180},3,[140,182,184],{"emptyLinePlaceholder":183},true,"\n",[140,186,188],{"class":142,"line":187},4,[140,189,190],{"class":146},"\u002F\u002F Virtual thread — JVM-managed, cheap:\n",[140,192,194,197,199,202,205,208,211,213,215,217],{"class":142,"line":193},5,[140,195,196],{"class":153},"Thread virtual ",[140,198,158],{"class":157},[140,200,201],{"class":153}," Thread.",[140,203,204],{"class":164},"ofVirtual",[140,206,207],{"class":153},"().",[140,209,210],{"class":164},"start",[140,212,168],{"class":153},[140,214,171],{"class":157},[140,216,174],{"class":164},[140,218,177],{"class":153},[10,220,222],{"id":221},"carrier-threads-the-runtime-model","Carrier threads — the runtime model",[15,224,225,226,229],{},"The JVM keeps a small ",[29,227,228],{},"carrier thread pool"," — a ForkJoinPool whose size defaults to\nthe number of available CPU cores. Virtual threads are scheduled onto carrier threads:",[231,232,233,241,256],"ol",{},[234,235,236,237,240],"li",{},"A virtual thread ",[29,238,239],{},"mounts"," onto a carrier thread and executes.",[234,242,243,244,247,248,251,252,255],{},"When it hits a blocking operation (I\u002FO, ",[22,245,246],{},"sleep",", ",[22,249,250],{},"LockSupport.park","), the JVM\n",[29,253,254],{},"unmounts"," the virtual thread: its stack is saved to the heap, and the carrier\nthread is released to run another virtual thread.",[234,257,258,259,262],{},"When the blocking operation completes, the JVM ",[29,260,261],{},"remounts"," the virtual thread on\nany available carrier thread and resumes from where it left off.",[131,264,269],{"className":265,"code":267,"language":268},[266],"language-text","Carrier pool:    [C1]  [C2]  [C3]  [C4]    ← 4 OS threads\nVirtual threads: [V1]  [V2]  ...  [V50000]  ← 50,000 concurrent tasks\n\nV1 blocks on DB query → unmounted from C1 → C1 immediately runs V2\nV1 DB result arrives  → V1 remounts on C3 → continues processing\n","text",[22,270,267],{"__ignoreMap":136},[15,272,273],{},"From V1's perspective nothing unusual happened — it \"blocked\" on a DB call just like\nalways. The JVM handled the parking transparently.",[10,275,277],{"id":276},"thread-per-request-at-scale","Thread-per-request at scale",[15,279,280,281,284],{},"Virtual threads make the ",[29,282,283],{},"thread-per-request model"," practical for high-concurrency\nservers. Instead of complex async APIs, each request handler is a plain synchronous\nmethod:",[131,286,288],{"className":133,"code":287,"language":135,"meta":136,"style":136},"\u002F\u002F Spring Boot 3.2+ — one line to enable:\n\u002F\u002F spring.threads.virtual.enabled=true\n\n\u002F\u002F Or manually:\ntry (var executor = Executors.newVirtualThreadPerTaskExecutor()) {\n    for (HttpRequest req : incomingRequests) {\n        executor.submit(() -> handleRequest(req)); \u002F\u002F each request gets a virtual thread\n    }\n}\n\nvoid handleRequest(HttpRequest req) {\n    User user = db.findUser(req.userId());       \u002F\u002F blocking JDBC — fine\n    List\u003COrder> orders = db.findOrders(user.id()); \u002F\u002F another blocking call — fine\n    sendResponse(new Response(user, orders));\n}\n",[22,289,290,295,300,304,309,334,349,371,377,383,388,399,425,456,474],{"__ignoreMap":136},[140,291,292],{"class":142,"line":143},[140,293,294],{"class":146},"\u002F\u002F Spring Boot 3.2+ — one line to enable:\n",[140,296,297],{"class":142,"line":150},[140,298,299],{"class":146},"\u002F\u002F spring.threads.virtual.enabled=true\n",[140,301,302],{"class":142,"line":180},[140,303,184],{"emptyLinePlaceholder":183},[140,305,306],{"class":142,"line":187},[140,307,308],{"class":146},"\u002F\u002F Or manually:\n",[140,310,311,314,317,320,323,325,328,331],{"class":142,"line":193},[140,312,313],{"class":157},"try",[140,315,316],{"class":153}," (",[140,318,319],{"class":157},"var",[140,321,322],{"class":153}," executor ",[140,324,158],{"class":157},[140,326,327],{"class":153}," Executors.",[140,329,330],{"class":164},"newVirtualThreadPerTaskExecutor",[140,332,333],{"class":153},"()) {\n",[140,335,337,340,343,346],{"class":142,"line":336},6,[140,338,339],{"class":157},"    for",[140,341,342],{"class":153}," (HttpRequest req ",[140,344,345],{"class":157},":",[140,347,348],{"class":153}," incomingRequests) {\n",[140,350,352,355,358,360,362,365,368],{"class":142,"line":351},7,[140,353,354],{"class":153},"        executor.",[140,356,357],{"class":164},"submit",[140,359,168],{"class":153},[140,361,171],{"class":157},[140,363,364],{"class":164}," handleRequest",[140,366,367],{"class":153},"(req)); ",[140,369,370],{"class":146},"\u002F\u002F each request gets a virtual thread\n",[140,372,374],{"class":142,"line":373},8,[140,375,376],{"class":153},"    }\n",[140,378,380],{"class":142,"line":379},9,[140,381,382],{"class":153},"}\n",[140,384,386],{"class":142,"line":385},10,[140,387,184],{"emptyLinePlaceholder":183},[140,389,391,394,396],{"class":142,"line":390},11,[140,392,393],{"class":157},"void",[140,395,364],{"class":164},[140,397,398],{"class":153},"(HttpRequest req) {\n",[140,400,402,405,407,410,413,416,419,422],{"class":142,"line":401},12,[140,403,404],{"class":153},"    User user ",[140,406,158],{"class":157},[140,408,409],{"class":153}," db.",[140,411,412],{"class":164},"findUser",[140,414,415],{"class":153},"(req.",[140,417,418],{"class":164},"userId",[140,420,421],{"class":153},"());       ",[140,423,424],{"class":146},"\u002F\u002F blocking JDBC — fine\n",[140,426,428,431,434,437,439,441,444,447,450,453],{"class":142,"line":427},13,[140,429,430],{"class":153},"    List\u003C",[140,432,433],{"class":157},"Order",[140,435,436],{"class":153},"> orders ",[140,438,158],{"class":157},[140,440,409],{"class":153},[140,442,443],{"class":164},"findOrders",[140,445,446],{"class":153},"(user.",[140,448,449],{"class":164},"id",[140,451,452],{"class":153},"()); ",[140,454,455],{"class":146},"\u002F\u002F another blocking call — fine\n",[140,457,459,462,465,468,471],{"class":142,"line":458},14,[140,460,461],{"class":164},"    sendResponse",[140,463,464],{"class":153},"(",[140,466,467],{"class":157},"new",[140,469,470],{"class":164}," Response",[140,472,473],{"class":153},"(user, orders));\n",[140,475,477],{"class":142,"line":476},15,[140,478,382],{"class":153},[15,480,481,482,485],{},"The JDBC calls block the virtual thread, but the carrier thread is freed during the wait.\n50,000 concurrent requests use only ",[22,483,484],{},"num_CPUs"," OS threads for CPU work, plus the I\u002FO\nthread pool of the underlying networking library.",[10,487,489],{"id":488},"creating-virtual-threads","Creating virtual threads",[131,491,493],{"className":133,"code":492,"language":135,"meta":136,"style":136},"\u002F\u002F Thread.ofVirtual() builder:\nThread vt = Thread.ofVirtual()\n                  .name(\"request-handler-\", 0)  \u002F\u002F numbered names\n                  .start(task);\n\n\u002F\u002F Thread.startVirtualThread() — shorthand:\nThread vt2 = Thread.startVirtualThread(task);\n\n\u002F\u002F ExecutorService (recommended for servers):\ntry (ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor()) {\n    exec.submit(task1);\n    exec.submit(task2);\n} \u002F\u002F blocks until all tasks finish, then closes\n\n\u002F\u002F ThreadFactory for framework integration:\nThreadFactory factory = Thread.ofVirtual().factory();\n",[22,494,495,500,514,540,549,553,558,572,576,581,596,606,615,623,627,632],{"__ignoreMap":136},[140,496,497],{"class":142,"line":143},[140,498,499],{"class":146},"\u002F\u002F Thread.ofVirtual() builder:\n",[140,501,502,505,507,509,511],{"class":142,"line":150},[140,503,504],{"class":153},"Thread vt ",[140,506,158],{"class":157},[140,508,201],{"class":153},[140,510,204],{"class":164},[140,512,513],{"class":153},"()\n",[140,515,516,519,522,524,528,530,534,537],{"class":142,"line":180},[140,517,518],{"class":153},"                  .",[140,520,521],{"class":164},"name",[140,523,464],{"class":153},[140,525,527],{"class":526},"sZZnC","\"request-handler-\"",[140,529,247],{"class":153},[140,531,533],{"class":532},"sj4cs","0",[140,535,536],{"class":153},")  ",[140,538,539],{"class":146},"\u002F\u002F numbered names\n",[140,541,542,544,546],{"class":142,"line":187},[140,543,518],{"class":153},[140,545,210],{"class":164},[140,547,548],{"class":153},"(task);\n",[140,550,551],{"class":142,"line":193},[140,552,184],{"emptyLinePlaceholder":183},[140,554,555],{"class":142,"line":336},[140,556,557],{"class":146},"\u002F\u002F Thread.startVirtualThread() — shorthand:\n",[140,559,560,563,565,567,570],{"class":142,"line":351},[140,561,562],{"class":153},"Thread vt2 ",[140,564,158],{"class":157},[140,566,201],{"class":153},[140,568,569],{"class":164},"startVirtualThread",[140,571,548],{"class":153},[140,573,574],{"class":142,"line":373},[140,575,184],{"emptyLinePlaceholder":183},[140,577,578],{"class":142,"line":379},[140,579,580],{"class":146},"\u002F\u002F ExecutorService (recommended for servers):\n",[140,582,583,585,588,590,592,594],{"class":142,"line":385},[140,584,313],{"class":157},[140,586,587],{"class":153}," (ExecutorService exec ",[140,589,158],{"class":157},[140,591,327],{"class":153},[140,593,330],{"class":164},[140,595,333],{"class":153},[140,597,598,601,603],{"class":142,"line":390},[140,599,600],{"class":153},"    exec.",[140,602,357],{"class":164},[140,604,605],{"class":153},"(task1);\n",[140,607,608,610,612],{"class":142,"line":401},[140,609,600],{"class":153},[140,611,357],{"class":164},[140,613,614],{"class":153},"(task2);\n",[140,616,617,620],{"class":142,"line":427},[140,618,619],{"class":153},"} ",[140,621,622],{"class":146},"\u002F\u002F blocks until all tasks finish, then closes\n",[140,624,625],{"class":142,"line":458},[140,626,184],{"emptyLinePlaceholder":183},[140,628,629],{"class":142,"line":476},[140,630,631],{"class":146},"\u002F\u002F ThreadFactory for framework integration:\n",[140,633,635,638,640,642,644,646,649],{"class":142,"line":634},16,[140,636,637],{"class":153},"ThreadFactory factory ",[140,639,158],{"class":157},[140,641,201],{"class":153},[140,643,204],{"class":164},[140,645,207],{"class":153},[140,647,648],{"class":164},"factory",[140,650,651],{"class":153},"();\n",[15,653,654],{},"For Spring Boot 3.2+, Tomcat 10.1+, Jetty 12+: a single property enables virtual threads\nglobally without code changes.",[10,656,658],{"id":657},"pinning-the-main-gotcha","Pinning — the main gotcha",[15,660,661,664],{},[29,662,663],{},"Pinning"," is when a virtual thread cannot be unmounted from its carrier during a\nblocking call. Pinned virtual threads hold an OS thread idle — exactly what virtual\nthreads are supposed to prevent.",[15,666,667],{},"Pinning occurs in two situations:",[669,670,672,673,676],"h3",{"id":671},"_1-synchronized-blocks-containing-blocking-operations","1. ",[22,674,675],{},"synchronized"," blocks containing blocking operations",[131,678,680],{"className":133,"code":679,"language":135,"meta":136,"style":136},"\u002F\u002F BAD — virtual thread is pinned for the entire sleep:\nsynchronized (lock) {\n    fetchFromDatabase(); \u002F\u002F blocks — carrier thread held idle!\n}\n\n\u002F\u002F GOOD — ReentrantLock parks correctly:\nreentrantLock.lock();\ntry {\n    fetchFromDatabase(); \u002F\u002F virtual thread unmounts; carrier freed\n} finally {\n    reentrantLock.unlock();\n}\n",[22,681,682,687,694,705,709,713,718,728,735,744,753,763],{"__ignoreMap":136},[140,683,684],{"class":142,"line":143},[140,685,686],{"class":146},"\u002F\u002F BAD — virtual thread is pinned for the entire sleep:\n",[140,688,689,691],{"class":142,"line":150},[140,690,675],{"class":157},[140,692,693],{"class":153}," (lock) {\n",[140,695,696,699,702],{"class":142,"line":180},[140,697,698],{"class":164},"    fetchFromDatabase",[140,700,701],{"class":153},"(); ",[140,703,704],{"class":146},"\u002F\u002F blocks — carrier thread held idle!\n",[140,706,707],{"class":142,"line":187},[140,708,382],{"class":153},[140,710,711],{"class":142,"line":193},[140,712,184],{"emptyLinePlaceholder":183},[140,714,715],{"class":142,"line":336},[140,716,717],{"class":146},"\u002F\u002F GOOD — ReentrantLock parks correctly:\n",[140,719,720,723,726],{"class":142,"line":351},[140,721,722],{"class":153},"reentrantLock.",[140,724,725],{"class":164},"lock",[140,727,651],{"class":153},[140,729,730,732],{"class":142,"line":373},[140,731,313],{"class":157},[140,733,734],{"class":153}," {\n",[140,736,737,739,741],{"class":142,"line":379},[140,738,698],{"class":164},[140,740,701],{"class":153},[140,742,743],{"class":146},"\u002F\u002F virtual thread unmounts; carrier freed\n",[140,745,746,748,751],{"class":142,"line":385},[140,747,619],{"class":153},[140,749,750],{"class":157},"finally",[140,752,734],{"class":153},[140,754,755,758,761],{"class":142,"line":390},[140,756,757],{"class":153},"    reentrantLock.",[140,759,760],{"class":164},"unlock",[140,762,651],{"class":153},[140,764,765],{"class":142,"line":401},[140,766,382],{"class":153},[669,768,770],{"id":769},"_2-native-method-frames-jni","2. Native method frames (JNI)",[15,772,773],{},"If a virtual thread calls a native method that blocks, the JVM cannot save its frame to\nthe heap, so it is pinned.",[15,775,776],{},[29,777,778],{},"Detect pinning:",[131,780,784],{"className":781,"code":782,"language":783,"meta":136,"style":136},"language-bash shiki shiki-themes github-light github-dark","java -Djdk.tracePinnedThreads=full MyApp\n# Logs a stack trace every time a virtual thread becomes pinned\n","bash",[22,785,786,796],{"__ignoreMap":136},[140,787,788,790,793],{"class":142,"line":143},[140,789,135],{"class":164},[140,791,792],{"class":532}," -Djdk.tracePinnedThreads=full",[140,794,795],{"class":526}," MyApp\n",[140,797,798],{"class":142,"line":150},[140,799,800],{"class":146},"# Logs a stack trace every time a virtual thread becomes pinned\n",[15,802,803,804,806,807,809,810,813],{},"Java 24 is actively working to eliminate ",[22,805,675],{},"-based pinning in most cases,\nbut until then, replace ",[22,808,675],{}," blocks that contain blocking I\u002FO with\n",[22,811,812],{},"ReentrantLock",".",[10,815,817],{"id":816},"threadlocal-and-virtual-threads","ThreadLocal and virtual threads",[15,819,820,823,824,826],{},[22,821,822],{},"ThreadLocal"," works with virtual threads but has scaling issues: if each of a million\nvirtual threads initializes a heavy ",[22,825,822],{}," (a DB connection, a large buffer),\nmemory explodes. Virtual threads are not pooled, so values don't leak between tasks —\nbut the per-thread allocation cost remains.",[15,828,829,832],{},[29,830,831],{},"ScopedValue"," (Java 21, JEP 446) is the virtual-thread-friendly replacement:",[131,834,836],{"className":133,"code":835,"language":135,"meta":136,"style":136},"\u002F\u002F ThreadLocal — one instance per thread, including virtual threads:\nstatic final ThreadLocal\u003CUser> CURRENT_USER = new ThreadLocal\u003C>();\nCURRENT_USER.set(user);            \u002F\u002F must remember to remove() later\n\u002F\u002F ... elsewhere:\nUser u = CURRENT_USER.get();\nCURRENT_USER.remove();             \u002F\u002F easy to forget → memory leak\n\n\u002F\u002F ScopedValue — immutable, inheritable, auto-cleaned:\nstatic final ScopedValue\u003CUser> CURRENT_USER = ScopedValue.newInstance();\n\nScopedValue.where(CURRENT_USER, user).run(() -> {\n    processRequest();              \u002F\u002F CURRENT_USER.get() == user within scope\n});                                \u002F\u002F cleaned up automatically on scope exit\n",[22,837,838,843,867,881,886,901,914,918,923,946,950,970,981],{"__ignoreMap":136},[140,839,840],{"class":142,"line":143},[140,841,842],{"class":146},"\u002F\u002F ThreadLocal — one instance per thread, including virtual threads:\n",[140,844,845,848,851,854,857,860,862,864],{"class":142,"line":150},[140,846,847],{"class":157},"static",[140,849,850],{"class":157}," final",[140,852,853],{"class":153}," ThreadLocal\u003C",[140,855,856],{"class":157},"User",[140,858,859],{"class":153},"> CURRENT_USER ",[140,861,158],{"class":157},[140,863,161],{"class":157},[140,865,866],{"class":153}," ThreadLocal\u003C>();\n",[140,868,869,872,875,878],{"class":142,"line":180},[140,870,871],{"class":153},"CURRENT_USER.",[140,873,874],{"class":164},"set",[140,876,877],{"class":153},"(user);            ",[140,879,880],{"class":146},"\u002F\u002F must remember to remove() later\n",[140,882,883],{"class":142,"line":187},[140,884,885],{"class":146},"\u002F\u002F ... elsewhere:\n",[140,887,888,891,893,896,899],{"class":142,"line":193},[140,889,890],{"class":153},"User u ",[140,892,158],{"class":157},[140,894,895],{"class":153}," CURRENT_USER.",[140,897,898],{"class":164},"get",[140,900,651],{"class":153},[140,902,903,905,908,911],{"class":142,"line":336},[140,904,871],{"class":153},[140,906,907],{"class":164},"remove",[140,909,910],{"class":153},"();             ",[140,912,913],{"class":146},"\u002F\u002F easy to forget → memory leak\n",[140,915,916],{"class":142,"line":351},[140,917,184],{"emptyLinePlaceholder":183},[140,919,920],{"class":142,"line":373},[140,921,922],{"class":146},"\u002F\u002F ScopedValue — immutable, inheritable, auto-cleaned:\n",[140,924,925,927,929,932,934,936,938,941,944],{"class":142,"line":379},[140,926,847],{"class":157},[140,928,850],{"class":157},[140,930,931],{"class":153}," ScopedValue\u003C",[140,933,856],{"class":157},[140,935,859],{"class":153},[140,937,158],{"class":157},[140,939,940],{"class":153}," ScopedValue.",[140,942,943],{"class":164},"newInstance",[140,945,651],{"class":153},[140,947,948],{"class":142,"line":385},[140,949,184],{"emptyLinePlaceholder":183},[140,951,952,955,958,961,964,966,968],{"class":142,"line":390},[140,953,954],{"class":153},"ScopedValue.",[140,956,957],{"class":164},"where",[140,959,960],{"class":153},"(CURRENT_USER, user).",[140,962,963],{"class":164},"run",[140,965,168],{"class":153},[140,967,171],{"class":157},[140,969,734],{"class":153},[140,971,972,975,978],{"class":142,"line":401},[140,973,974],{"class":164},"    processRequest",[140,976,977],{"class":153},"();              ",[140,979,980],{"class":146},"\u002F\u002F CURRENT_USER.get() == user within scope\n",[140,982,983,986],{"class":142,"line":427},[140,984,985],{"class":153},"});                                ",[140,987,988],{"class":146},"\u002F\u002F cleaned up automatically on scope exit\n",[15,990,991,993],{},[22,992,831],{}," is immutable within a scope (you can't accidentally overwrite it), inherits\ninto child threads automatically, and requires no cleanup.",[10,995,997],{"id":996},"structured-concurrency","Structured concurrency",[15,999,1000,1002],{},[29,1001,997],{}," (Java 21 preview, JEP 453) ensures subtask lifetimes are\nbounded by the parent scope:",[131,1004,1006],{"className":133,"code":1005,"language":135,"meta":136,"style":136},"try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {\n    Future\u003CUser>        user   = scope.fork(() -> db.findUser(id));\n    Future\u003CList\u003COrder>> orders = scope.fork(() -> db.findOrders(id));\n\n    scope.join();            \u002F\u002F waits for both forks\n    scope.throwIfFailed();   \u002F\u002F throws if either fork threw\n\n    return new Response(user.resultNow(), orders.resultNow());\n}\n\u002F\u002F At this point: both tasks are guaranteed done or cancelled\n",[22,1007,1008,1031,1060,1086,1090,1104,1117,1121,1142,1146],{"__ignoreMap":136},[140,1009,1010,1012,1014,1016,1019,1021,1023,1026,1029],{"class":142,"line":143},[140,1011,313],{"class":157},[140,1013,316],{"class":153},[140,1015,319],{"class":157},[140,1017,1018],{"class":153}," scope ",[140,1020,158],{"class":157},[140,1022,161],{"class":157},[140,1024,1025],{"class":153}," StructuredTaskScope.",[140,1027,1028],{"class":164},"ShutdownOnFailure",[140,1030,333],{"class":153},[140,1032,1033,1036,1038,1041,1043,1046,1049,1051,1053,1055,1057],{"class":142,"line":150},[140,1034,1035],{"class":153},"    Future\u003C",[140,1037,856],{"class":157},[140,1039,1040],{"class":153},">        user   ",[140,1042,158],{"class":157},[140,1044,1045],{"class":153}," scope.",[140,1047,1048],{"class":164},"fork",[140,1050,168],{"class":153},[140,1052,171],{"class":157},[140,1054,409],{"class":153},[140,1056,412],{"class":164},[140,1058,1059],{"class":153},"(id));\n",[140,1061,1062,1065,1067,1070,1072,1074,1076,1078,1080,1082,1084],{"class":142,"line":180},[140,1063,1064],{"class":153},"    Future\u003CList\u003C",[140,1066,433],{"class":157},[140,1068,1069],{"class":153},">> orders ",[140,1071,158],{"class":157},[140,1073,1045],{"class":153},[140,1075,1048],{"class":164},[140,1077,168],{"class":153},[140,1079,171],{"class":157},[140,1081,409],{"class":153},[140,1083,443],{"class":164},[140,1085,1059],{"class":153},[140,1087,1088],{"class":142,"line":187},[140,1089,184],{"emptyLinePlaceholder":183},[140,1091,1092,1095,1098,1101],{"class":142,"line":193},[140,1093,1094],{"class":153},"    scope.",[140,1096,1097],{"class":164},"join",[140,1099,1100],{"class":153},"();            ",[140,1102,1103],{"class":146},"\u002F\u002F waits for both forks\n",[140,1105,1106,1108,1111,1114],{"class":142,"line":336},[140,1107,1094],{"class":153},[140,1109,1110],{"class":164},"throwIfFailed",[140,1112,1113],{"class":153},"();   ",[140,1115,1116],{"class":146},"\u002F\u002F throws if either fork threw\n",[140,1118,1119],{"class":142,"line":351},[140,1120,184],{"emptyLinePlaceholder":183},[140,1122,1123,1126,1128,1130,1132,1135,1138,1140],{"class":142,"line":373},[140,1124,1125],{"class":157},"    return",[140,1127,161],{"class":157},[140,1129,470],{"class":164},[140,1131,446],{"class":153},[140,1133,1134],{"class":164},"resultNow",[140,1136,1137],{"class":153},"(), orders.",[140,1139,1134],{"class":164},[140,1141,177],{"class":153},[140,1143,1144],{"class":142,"line":379},[140,1145,382],{"class":153},[140,1147,1148],{"class":142,"line":385},[140,1149,1150],{"class":146},"\u002F\u002F At this point: both tasks are guaranteed done or cancelled\n",[15,1152,1153,1155,1156,1159],{},[22,1154,1028],{}," cancels remaining subtasks when one fails. ",[22,1157,1158],{},"ShutdownOnSuccess","\nreturns as soon as any succeeds — useful for parallel hedging (run the same query on two\nreplicas, take the first response).",[15,1161,1162],{},"Without structured concurrency, a thrown exception in one fork would let the other fork\nrun forever — a thread leak. The scope prevents this.",[10,1164,1166],{"id":1165},"when-not-to-use-virtual-threads","When NOT to use virtual threads",[15,1168,1169],{},"Virtual threads are not universally better than platform threads:",[15,1171,1172,1175,1176,1179,1180,813],{},[29,1173,1174],{},"CPU-bound work"," — a CPU-intensive computation occupies a carrier thread for its entire\nrun. Hundreds of CPU-bound virtual threads compete for the same small carrier pool and\nstarve each other. Use ",[22,1177,1178],{},"ForkJoinPool.commonPool()"," or ",[22,1181,1182],{},"newFixedThreadPool(nCPUs)",[15,1184,1185,1188,1189,1192],{},[29,1186,1187],{},"Code relying on thread identity"," — some libraries use ",[22,1190,1191],{},"Thread.currentThread()"," as a\nmap key or assume ThreadLocal values persist across tasks in a pool. Virtual threads\naren't pooled, so these assumptions break.",[15,1194,1195,1198],{},[29,1196,1197],{},"Long synchronized\u002Fnative sections"," — pinning turns a virtual thread into an expensive\nplatform thread for the duration.",[131,1200,1202],{"className":133,"code":1201,"language":135,"meta":136,"style":136},"\u002F\u002F CPU-bound — use platform threads:\nvar pool = ForkJoinPool.commonPool();\npool.submit(() -> encodeVideo(source, dest)); \u002F\u002F needs all CPU time it can get\n\n\u002F\u002F I\u002FO-bound — use virtual threads:\nvar exec = Executors.newVirtualThreadPerTaskExecutor();\nexec.submit(() -> httpClient.get(url));       \u002F\u002F spends most time waiting\n",[22,1203,1204,1209,1226,1246,1250,1255,1270],{"__ignoreMap":136},[140,1205,1206],{"class":142,"line":143},[140,1207,1208],{"class":146},"\u002F\u002F CPU-bound — use platform threads:\n",[140,1210,1211,1213,1216,1218,1221,1224],{"class":142,"line":150},[140,1212,319],{"class":157},[140,1214,1215],{"class":153}," pool ",[140,1217,158],{"class":157},[140,1219,1220],{"class":153}," ForkJoinPool.",[140,1222,1223],{"class":164},"commonPool",[140,1225,651],{"class":153},[140,1227,1228,1231,1233,1235,1237,1240,1243],{"class":142,"line":180},[140,1229,1230],{"class":153},"pool.",[140,1232,357],{"class":164},[140,1234,168],{"class":153},[140,1236,171],{"class":157},[140,1238,1239],{"class":164}," encodeVideo",[140,1241,1242],{"class":153},"(source, dest)); ",[140,1244,1245],{"class":146},"\u002F\u002F needs all CPU time it can get\n",[140,1247,1248],{"class":142,"line":187},[140,1249,184],{"emptyLinePlaceholder":183},[140,1251,1252],{"class":142,"line":193},[140,1253,1254],{"class":146},"\u002F\u002F I\u002FO-bound — use virtual threads:\n",[140,1256,1257,1259,1262,1264,1266,1268],{"class":142,"line":336},[140,1258,319],{"class":157},[140,1260,1261],{"class":153}," exec ",[140,1263,158],{"class":157},[140,1265,327],{"class":153},[140,1267,330],{"class":164},[140,1269,651],{"class":153},[140,1271,1272,1275,1277,1279,1281,1284,1286,1289],{"class":142,"line":351},[140,1273,1274],{"class":153},"exec.",[140,1276,357],{"class":164},[140,1278,168],{"class":153},[140,1280,171],{"class":157},[140,1282,1283],{"class":153}," httpClient.",[140,1285,898],{"class":164},[140,1287,1288],{"class":153},"(url));       ",[140,1290,1291],{"class":146},"\u002F\u002F spends most time waiting\n",[10,1293,1295],{"id":1294},"virtual-threads-vs-reactive-programming","Virtual threads vs reactive programming",[15,1297,1298],{},"Reactive frameworks (Project Reactor, RxJava) solved the same I\u002FO-bound scaling problem\nby making every operation non-blocking and composing chains of transformations:",[131,1300,1302],{"className":133,"code":1301,"language":135,"meta":136,"style":136},"\u002F\u002F Reactor — non-blocking, complex composition:\nMono.fromCallable(() -> db.findUser(id))\n    .subscribeOn(Schedulers.boundedElastic())\n    .flatMap(user -> db.findOrders(user.id()))\n    .map(orders -> new Response(user, orders))\n    .subscribe(response -> send(response), err -> sendError(err));\n\n\u002F\u002F Virtual threads — blocking, sequential, simple:\nUser user = db.findUser(id);           \u002F\u002F blocks virtual thread\nList\u003COrder> orders = db.findOrders(user.id());\nreturn new Response(user, orders);\n",[22,1303,1304,1309,1328,1345,1368,1387,1413,1417,1422,1439,1460],{"__ignoreMap":136},[140,1305,1306],{"class":142,"line":143},[140,1307,1308],{"class":146},"\u002F\u002F Reactor — non-blocking, complex composition:\n",[140,1310,1311,1314,1317,1319,1321,1323,1325],{"class":142,"line":150},[140,1312,1313],{"class":153},"Mono.",[140,1315,1316],{"class":164},"fromCallable",[140,1318,168],{"class":153},[140,1320,171],{"class":157},[140,1322,409],{"class":153},[140,1324,412],{"class":164},[140,1326,1327],{"class":153},"(id))\n",[140,1329,1330,1333,1336,1339,1342],{"class":142,"line":180},[140,1331,1332],{"class":153},"    .",[140,1334,1335],{"class":164},"subscribeOn",[140,1337,1338],{"class":153},"(Schedulers.",[140,1340,1341],{"class":164},"boundedElastic",[140,1343,1344],{"class":153},"())\n",[140,1346,1347,1349,1352,1355,1357,1359,1361,1363,1365],{"class":142,"line":187},[140,1348,1332],{"class":153},[140,1350,1351],{"class":164},"flatMap",[140,1353,1354],{"class":153},"(user ",[140,1356,171],{"class":157},[140,1358,409],{"class":153},[140,1360,443],{"class":164},[140,1362,446],{"class":153},[140,1364,449],{"class":164},[140,1366,1367],{"class":153},"()))\n",[140,1369,1370,1372,1375,1378,1380,1382,1384],{"class":142,"line":193},[140,1371,1332],{"class":153},[140,1373,1374],{"class":164},"map",[140,1376,1377],{"class":153},"(orders ",[140,1379,171],{"class":157},[140,1381,161],{"class":157},[140,1383,470],{"class":164},[140,1385,1386],{"class":153},"(user, orders))\n",[140,1388,1389,1391,1394,1397,1399,1402,1405,1407,1410],{"class":142,"line":336},[140,1390,1332],{"class":153},[140,1392,1393],{"class":164},"subscribe",[140,1395,1396],{"class":153},"(response ",[140,1398,171],{"class":157},[140,1400,1401],{"class":164}," send",[140,1403,1404],{"class":153},"(response), err ",[140,1406,171],{"class":157},[140,1408,1409],{"class":164}," sendError",[140,1411,1412],{"class":153},"(err));\n",[140,1414,1415],{"class":142,"line":351},[140,1416,184],{"emptyLinePlaceholder":183},[140,1418,1419],{"class":142,"line":373},[140,1420,1421],{"class":146},"\u002F\u002F Virtual threads — blocking, sequential, simple:\n",[140,1423,1424,1427,1429,1431,1433,1436],{"class":142,"line":379},[140,1425,1426],{"class":153},"User user ",[140,1428,158],{"class":157},[140,1430,409],{"class":153},[140,1432,412],{"class":164},[140,1434,1435],{"class":153},"(id);           ",[140,1437,1438],{"class":146},"\u002F\u002F blocks virtual thread\n",[140,1440,1441,1444,1446,1448,1450,1452,1454,1456,1458],{"class":142,"line":385},[140,1442,1443],{"class":153},"List\u003C",[140,1445,433],{"class":157},[140,1447,436],{"class":153},[140,1449,158],{"class":157},[140,1451,409],{"class":153},[140,1453,443],{"class":164},[140,1455,446],{"class":153},[140,1457,449],{"class":164},[140,1459,177],{"class":153},[140,1461,1462,1465,1467,1469],{"class":142,"line":390},[140,1463,1464],{"class":157},"return",[140,1466,161],{"class":157},[140,1468,470],{"class":164},[140,1470,1471],{"class":153},"(user, orders);\n",[15,1473,1474],{},"For most I\u002FO-bound services, virtual threads eliminate the need for reactive. Reactive\nstill makes sense for:",[1476,1477,1478,1484,1490],"ul",{},[234,1479,1480,1483],{},[29,1481,1482],{},"Backpressure"," — controlling producer\u002Fconsumer throughput.",[234,1485,1486,1489],{},[29,1487,1488],{},"Streaming"," — processing a data stream element by element.",[234,1491,1492,1495],{},[29,1493,1494],{},"Existing reactive ecosystems"," (Vert.x, Quarkus reactive mode).",[10,1497,1499],{"id":1498},"observability","Observability",[131,1501,1503],{"className":781,"code":1502,"language":783,"meta":136,"style":136},"# Thread dump — includes virtual threads:\njcmd \u003Cpid> Thread.dump_to_file -format=json \u002Ftmp\u002Fthreads.json\n\n# JFR recording — virtual thread lifecycle, pinning events:\njava -XX:StartFlightRecording=filename=app.jfr,settings=profile MyApp\n\n# Detect pinning:\njava -Djdk.tracePinnedThreads=full MyApp\n\n# Native memory tracking (carrier pool):\njava -XX:NativeMemoryTracking=summary MyApp\n",[22,1504,1505,1510,1536,1540,1545,1554,1558,1563,1571,1575,1580],{"__ignoreMap":136},[140,1506,1507],{"class":142,"line":143},[140,1508,1509],{"class":146},"# Thread dump — includes virtual threads:\n",[140,1511,1512,1515,1518,1521,1524,1527,1530,1533],{"class":142,"line":150},[140,1513,1514],{"class":164},"jcmd",[140,1516,1517],{"class":157}," \u003C",[140,1519,1520],{"class":526},"pi",[140,1522,1523],{"class":153},"d",[140,1525,1526],{"class":157},">",[140,1528,1529],{"class":526}," Thread.dump_to_file",[140,1531,1532],{"class":532}," -format=json",[140,1534,1535],{"class":526}," \u002Ftmp\u002Fthreads.json\n",[140,1537,1538],{"class":142,"line":180},[140,1539,184],{"emptyLinePlaceholder":183},[140,1541,1542],{"class":142,"line":187},[140,1543,1544],{"class":146},"# JFR recording — virtual thread lifecycle, pinning events:\n",[140,1546,1547,1549,1552],{"class":142,"line":193},[140,1548,135],{"class":164},[140,1550,1551],{"class":532}," -XX:StartFlightRecording=filename=app.jfr,settings=profile",[140,1553,795],{"class":526},[140,1555,1556],{"class":142,"line":336},[140,1557,184],{"emptyLinePlaceholder":183},[140,1559,1560],{"class":142,"line":351},[140,1561,1562],{"class":146},"# Detect pinning:\n",[140,1564,1565,1567,1569],{"class":142,"line":373},[140,1566,135],{"class":164},[140,1568,792],{"class":532},[140,1570,795],{"class":526},[140,1572,1573],{"class":142,"line":379},[140,1574,184],{"emptyLinePlaceholder":183},[140,1576,1577],{"class":142,"line":385},[140,1578,1579],{"class":146},"# Native memory tracking (carrier pool):\n",[140,1581,1582,1584,1587],{"class":142,"line":390},[140,1583,135],{"class":164},[140,1585,1586],{"class":532}," -XX:NativeMemoryTracking=summary",[140,1588,795],{"class":526},[15,1590,1591,1592,1595],{},"JFR emits ",[22,1593,1594],{},"jdk.VirtualThreadPinned"," events when pinning occurs. Thread dumps group\nvirtual threads with identical stack traces — 50,000 threads all blocked on JDBC appear\nas one entry with a count, not 50,000 individual stacks.",[10,1597,1599],{"id":1598},"migration-checklist","Migration checklist",[231,1601,1602,1615,1621,1632,1641,1651],{},[234,1603,1604,1607,1608,1611,1612,813],{},[29,1605,1606],{},"Replace the executor",": ",[22,1609,1610],{},"newFixedThreadPool(N)"," → ",[22,1613,1614],{},"newVirtualThreadPerTaskExecutor()",[234,1616,1617,1620],{},[29,1618,1619],{},"Remove artificial pool-size tuning"," — thread count limits are irrelevant for I\u002FO-bound tasks.",[234,1622,1623,1626,1627,1629,1630,813],{},[29,1624,1625],{},"Fix pinning",": audit ",[22,1628,675],{}," blocks that contain blocking I\u002FO; replace with ",[22,1631,812],{},[234,1633,1634,1637,1638,1640],{},[29,1635,1636],{},"Replace heavy ThreadLocals"," with ",[22,1639,831],{}," for context propagation (user, trace-id).",[234,1642,1643,1646,1647,1650],{},[29,1644,1645],{},"Load-test",": run under realistic load and check JFR for ",[22,1648,1649],{},"VirtualThreadPinned"," events.",[234,1652,1653,1656],{},[29,1654,1655],{},"Framework support",": update Spring Boot to 3.2+, Tomcat to 10.1+, Hibernate to 6.2+.",[10,1658,1660],{"id":1659},"recap","Recap",[15,1662,1663,1665,1666,1669,1670,1673,1674,1676,1677,1679,1680,1682,1683,1685,1686,1688,1689,1691],{},[29,1664,31],{}," (Java 21) are cheap, JVM-managed threads that mount onto a small\ncarrier thread pool and ",[29,1667,1668],{},"unmount during blocking I\u002FO",", freeing the carrier for other\nwork. This enables a ",[29,1671,1672],{},"thread-per-request"," model at millions-of-threads scale using\nplain blocking code. ",[29,1675,663],{}," — caused by ",[22,1678,675],{}," blocks holding monitors\nduring blocking calls or by JNI frames — defeats unmounting; replace with\n",[22,1681,812],{},". ",[29,1684,831],{}," replaces ",[22,1687,822],{}," for immutable context\npropagation. ",[29,1690,997],{}," bounds subtask lifetimes to the parent scope,\npreventing leaks and enabling clean cancellation. Virtual threads excel at I\u002FO-bound\nwork; keep platform threads for CPU-bound computation. For most new services they\neliminate the need for reactive programming, but reactive remains valuable for\nbackpressure and streaming use cases.",[1693,1694,1695],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":136,"searchDepth":150,"depth":150,"links":1697},[1698,1699,1700,1701,1702,1703,1708,1709,1710,1711,1712,1713,1714],{"id":12,"depth":150,"text":13},{"id":39,"depth":150,"text":40},{"id":221,"depth":150,"text":222},{"id":276,"depth":150,"text":277},{"id":488,"depth":150,"text":489},{"id":657,"depth":150,"text":658,"children":1704},[1705,1707],{"id":671,"depth":180,"text":1706},"1. synchronized blocks containing blocking operations",{"id":769,"depth":180,"text":770},{"id":816,"depth":150,"text":817},{"id":996,"depth":150,"text":997},{"id":1165,"depth":150,"text":1166},{"id":1294,"depth":150,"text":1295},{"id":1498,"depth":150,"text":1499},{"id":1598,"depth":150,"text":1599},{"id":1659,"depth":150,"text":1660},"Complete guide to Java virtual threads — how they differ from platform threads, carrier threads, the thread-per-request model, pinning, ThreadLocal vs ScopedValue, structured concurrency, when not to use virtual threads, migration from thread pools, and comparison with reactive programming.","hard","md","Java",{},"\u002Fblog\u002Fjava-virtual-threads","\u002Fjava\u002Fmodern-java\u002Fvirtual-threads",{"title":5,"description":1715},"blog\u002Fjava-virtual-threads","Virtual Threads","Modern Java","modern-java","2026-06-20","1Gx2pPzsXxLXYJsu6GR3yRyaU9DoKMF8jXiY3HMwdJ0",1782244091846]