[{"data":1,"prerenderedAt":118},["ShallowReactive",2],{"qa-\u002Fjava\u002Fmodern-java\u002Fvirtual-threads":3},{"page":4,"siblings":86,"blog":115},{"id":5,"title":6,"body":7,"description":11,"difficulty":14,"extension":15,"framework":16,"frameworkSlug":17,"meta":18,"navigation":19,"order":20,"path":21,"questions":22,"questionsCount":77,"related":78,"seo":79,"seoDescription":80,"stem":81,"subtopic":6,"topic":82,"topicSlug":83,"updated":84,"__hash__":85},"qa\u002Fjava\u002Fmodern-java\u002Fvirtual-threads.md","Virtual Threads",{"type":8,"value":9,"toc":10},"minimark",[],{"title":11,"searchDepth":12,"depth":12,"links":13},"",2,[],"hard","md","Java","java",{},true,6,"\u002Fjava\u002Fmodern-java\u002Fvirtual-threads",[23,28,33,37,41,45,49,53,57,61,65,69,73],{"id":24,"difficulty":25,"q":26,"a":27},"what-are-virtual-threads","easy","What are virtual threads in Java and what problem do they solve?","**Virtual threads** (Java 21, JEP 444, Project Loom) are lightweight\nthreads managed by the JVM rather than the OS. A traditional\n**platform thread** maps 1-to-1 to an OS thread — costly (~1 MB stack,\nslow to create, limited in number). A virtual thread is a JVM-managed\nconstruct that is **mounted onto a carrier platform thread** only when it\nneeds CPU time, and **unmounted** (parked) during blocking I\u002FO.\n\n```java\n\u002F\u002F Platform thread — one OS thread, ~1 MB stack:\nThread t = new Thread(task);\n\n\u002F\u002F Virtual thread — lightweight, ~few KB, millions possible:\nThread vt = Thread.ofVirtual().start(task);\n```\n\nThis enables a **thread-per-request** model where each HTTP request gets\nits own thread, but the JVM handles thousands of concurrent requests with\nonly a handful of OS threads underneath.\n\n**Rule of thumb:** virtual threads are not faster threads — they are\n*cheap* threads that eliminate the need to avoid blocking I\u002FO.\n",{"id":29,"difficulty":30,"q":31,"a":32},"platform-vs-virtual-threads","medium","How do virtual threads differ from platform threads?","| Aspect | Platform Thread | Virtual Thread |\n|---|---|---|\n| OS mapping | 1:1 with OS thread | Many-to-few (M:N) |\n| Stack size | ~1 MB (fixed) | ~few KB (growable) |\n| Creation cost | High (~ms) | Very low (~µs) |\n| Max concurrent | ~thousands | ~millions |\n| Blocking I\u002FO | Blocks the OS thread | JVM unmounts; carrier thread reused |\n| Thread pool needed | Yes, to limit overhead | Generally no |\n| `synchronized` | Full support | Risk of pinning (see pinning) |\n\n```java\n\u002F\u002F Platform threads need a pool to limit resource usage:\nExecutorService pool = Executors.newFixedThreadPool(200);\n\n\u002F\u002F Virtual threads: create freely per task\nExecutorService vExecutor = Executors.newVirtualThreadPerTaskExecutor();\n```\n\n**Rule of thumb:** virtual threads are the right default for I\u002FO-bound\nconcurrent tasks; platform threads remain necessary for CPU-bound work.\n",{"id":34,"difficulty":30,"q":35,"a":36},"carrier-threads","What is a carrier thread and how does the JVM schedule virtual threads?","A **carrier thread** is a platform thread that a virtual thread runs on.\nThe JVM maintains a small **ForkJoinPool** of carrier threads (defaulting\nto the number of CPU cores). Virtual threads are scheduled by the JVM onto\navailable carrier threads using **cooperative scheduling**:\n\n1. Virtual thread starts executing on a carrier thread.\n2. When it hits a blocking operation (I\u002FO, `sleep`, `LockSupport.park`),\n   the JVM **unmounts** the virtual thread from the carrier — saving its\n   stack to the heap.\n3. The carrier thread is now free to run another virtual thread.\n4. When the I\u002FO completes, the virtual thread is **remounted** on any\n   available carrier thread and resumes.\n\n```\nOS threads (carrier pool): [T1] [T2] [T3] [T4]\nVirtual threads:           [V1] [V2] ... [V10000]\nV1 blocks on I\u002FO → unmounted from T1 → T1 picks up V2 immediately\n```\n\n**Rule of thumb:** the carrier pool size is set by `-Djdk.virtualThreadScheduler.parallelism=N`\n(default = CPU cores); don't increase it for I\u002FO-bound workloads.\n",{"id":38,"difficulty":30,"q":39,"a":40},"thread-per-request-model","How do virtual threads enable the thread-per-request model?","Before virtual threads, thread-per-request was impractical beyond a few\nhundred concurrent requests because each request consumed an OS thread.\nFrameworks like Servlet, Spring MVC, and JDBC used **thread pools** and\n**reactive\u002Fasync** APIs to avoid blocking.\n\nWith virtual threads, the JVM handles the parking\u002Fscheduling:\n\n```java\n\u002F\u002F Spring Boot 3.2+ — enable virtual threads globally:\n\u002F\u002F spring.threads.virtual.enabled=true\n\n\u002F\u002F Or manually with Jakarta Servlets:\ntry (var executor = Executors.newVirtualThreadPerTaskExecutor()) {\n    executor.submit(() -> {\n        String data = jdbcTemplate.queryForObject(sql, String.class); \u002F\u002F blocks OK\n        return processData(data);\n    });\n}\n```\n\nEach request can use **blocking APIs** (JDBC, REST client, file I\u002FO)\nnaturally — the JVM suspends the virtual thread during the wait without\nconsuming an OS thread.\n\n**Rule of thumb:** virtual threads make simple synchronous blocking code\nscale as well as complex asynchronous reactive code — without callback\nhell or `CompletableFuture` chains.\n",{"id":42,"difficulty":14,"q":43,"a":44},"pinning","What is thread pinning in virtual threads and how do you avoid it?","**Pinning** occurs when a virtual thread cannot be unmounted from its\ncarrier thread during a blocking operation. This defeats the purpose of\nvirtual threads because the carrier OS thread is held blocked.\n\nTwo situations cause pinning:\n1. **`synchronized` blocks or methods** — the JVM cannot currently\n   unmount a virtual thread that holds a monitor lock.\n2. **Native methods** — JNI frames cannot be parked.\n\n```java\nsynchronized (lock) {\n    Thread.sleep(1000); \u002F\u002F virtual thread PINNED — carrier blocked for 1 sec\n}\n\n\u002F\u002F Fix: replace synchronized with ReentrantLock:\nlock.lock();\ntry {\n    Thread.sleep(1000); \u002F\u002F virtual thread parks correctly — carrier freed\n} finally {\n    lock.unlock();\n}\n```\n\nDetect pinning with: `-Djdk.tracePinnedThreads=full` (logs whenever\npinning occurs). Java 24+ is improving `synchronized` to avoid pinning\nin most cases.\n\n**Rule of thumb:** replace `synchronized` blocks that contain blocking\ncalls with `ReentrantLock`; avoid long-running native method calls on\nvirtual threads.\n",{"id":46,"difficulty":14,"q":47,"a":48},"threadlocal-virtual-threads","How does ThreadLocal behave with virtual threads and what is the alternative?","`ThreadLocal` works with virtual threads but has two problems at scale:\n\n1. If each virtual thread (potentially millions) initialises a heavy\n   `ThreadLocal` (e.g., a database connection), memory usage explodes.\n2. Thread pools reuse threads, so `ThreadLocal` values survive across\n   tasks — with virtual threads there's no pooling, so values don't leak\n   *between* tasks, but the allocation-per-thread cost remains.\n\nJava 20 introduced **Scoped Values** (`ScopedValue`, JEP 446 — finalized\nin Java 21) as the preferred alternative for virtual threads:\n\n```java\n\u002F\u002F ThreadLocal — allocated per virtual thread:\nprivate static final ThreadLocal\u003CUser> CURRENT_USER = new ThreadLocal\u003C>();\n\n\u002F\u002F ScopedValue — immutable, inherited by child threads, no per-thread alloc:\nprivate static final ScopedValue\u003CUser> CURRENT_USER = ScopedValue.newInstance();\n\nScopedValue.where(CURRENT_USER, user).run(() -> {\n    \u002F\u002F CURRENT_USER.get() returns 'user' within this scope\n    processRequest();\n});\n```\n\n`ScopedValue` is immutable and automatically cleaned up when the scope\nexits — no remove() needed, no accidental cross-task contamination.\n\n**Rule of thumb:** prefer `ScopedValue` over `ThreadLocal` for new code\non Java 21+, especially context propagation (user, trace-id, locale).\n",{"id":50,"difficulty":14,"q":51,"a":52},"structured-concurrency","What is structured concurrency and how does it relate to virtual threads?","**Structured concurrency** (Java 21 preview, JEP 453) ensures that when a\ntask spawns subtasks, their lifetimes are bounded by the parent task's\nscope. If the parent fails or is cancelled, subtasks are automatically\ncancelled — no orphaned threads.\n\n```java\ntry (var scope = new StructuredTaskScope.ShutdownOnFailure()) {\n    Future\u003CString> user    = scope.fork(() -> fetchUser(id));\n    Future\u003CList\u003COrder>> orders = scope.fork(() -> fetchOrders(id));\n\n    scope.join();           \u002F\u002F wait for both\n    scope.throwIfFailed();  \u002F\u002F propagate first failure\n\n    return new Response(user.resultNow(), orders.resultNow());\n}\n\u002F\u002F Scope exits → both subtasks guaranteed to be done or cancelled\n```\n\n`ShutdownOnFailure` cancels the remaining subtasks if one fails.\n`ShutdownOnSuccess` returns as soon as any subtask succeeds (useful for\nparallel search \u002F hedging).\n\n**Rule of thumb:** structured concurrency + virtual threads = simple\nsynchronous-looking code that is safe, cancellable, and leak-free.\n",{"id":54,"difficulty":30,"q":55,"a":56},"virtual-threads-not-for-cpu","When should you NOT use virtual threads?","Virtual threads are optimised for **I\u002FO-bound** work. They are not\nappropriate for:\n\n- **CPU-bound tasks** — a CPU-heavy virtual thread still occupies a\n  carrier thread for its entire run; many CPU-bound virtual threads compete\n  for the same carrier pool, starving each other. Use platform threads\n  in a `ForkJoinPool` or `newFixedThreadPool` instead.\n- **Code that relies on thread identity** — some frameworks or libraries\n  assume thread-pool reuse of `ThreadLocal`. Virtual threads don't pool,\n  so thread-identity assumptions break.\n- **Heavy synchronized sections over blocking I\u002FO** — pinning turns\n  virtual threads into expensive platform threads during those sections.\n\n```java\n\u002F\u002F Bad — CPU-bound on virtual threads starves the carrier pool:\nvar executor = Executors.newVirtualThreadPerTaskExecutor();\nexecutor.submit(() -> computePrimes(10_000_000)); \u002F\u002F blocks a carrier\n\n\u002F\u002F Better — CPU work on platform threads:\nvar pool = ForkJoinPool.commonPool();\npool.submit(() -> computePrimes(10_000_000));\n```\n\n**Rule of thumb:** virtual threads excel at I\u002FO-bound concurrent tasks;\nstick to platform thread pools for CPU-intensive computation.\n",{"id":58,"difficulty":25,"q":59,"a":60},"creating-virtual-threads","What are the different ways to create virtual threads in Java?","Java 21 provides several APIs for creating virtual threads:\n\n```java\n\u002F\u002F 1. Thread.ofVirtual() builder:\nThread vt = Thread.ofVirtual()\n                  .name(\"my-virtual-thread\")\n                  .start(myTask);\n\n\u002F\u002F 2. Thread.startVirtualThread() — shorthand:\nThread vt2 = Thread.startVirtualThread(myTask);\n\n\u002F\u002F 3. ExecutorService — one virtual thread per task:\ntry (ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor()) {\n    exec.submit(task1);\n    exec.submit(task2);\n} \u002F\u002F waits for all tasks on close\n\n\u002F\u002F 4. ThreadFactory for frameworks:\nThreadFactory factory = Thread.ofVirtual().factory();\n```\n\nSpring Boot 3.2+ and Tomcat 10.1.x+ support virtual threads with a single\nproperty: `spring.threads.virtual.enabled=true`.\n\n**Rule of thumb:** for servers, use `newVirtualThreadPerTaskExecutor()`\nor the framework's built-in virtual thread support; avoid creating raw\nvirtual threads manually in application code.\n",{"id":62,"difficulty":30,"q":63,"a":64},"virtual-threads-observability","How do you observe and debug virtual threads?","Standard observability tools work with virtual threads in Java 21+:\n\n```bash\n# Thread dump — shows virtual threads:\njcmd \u003Cpid> Thread.dump_to_file -format=json \u002Ftmp\u002Fthreads.json\n\n# JFR events — virtual thread mount\u002Funmount, pinning:\njava -XX:StartFlightRecording=filename=recording.jfr MyApp\n\n# Detect pinning at runtime:\njava -Djdk.tracePinnedThreads=full MyApp\n```\n\n`jstack` was updated to show virtual threads. JDK Flight Recorder (JFR)\nhas dedicated events for virtual thread lifecycle and pinning incidents.\n\nIn thread dumps, virtual threads are grouped and summarised by stack trace\nif many share the same waiting state (e.g., 50,000 threads all blocked on\nJDBC).\n\n**Rule of thumb:** enable JFR in production for virtual-thread apps;\nthe pinning events are the most important signal to watch for performance\nregressions.\n",{"id":66,"difficulty":30,"q":67,"a":68},"virtual-threads-migration","How do you migrate an existing thread-pool application to virtual threads?","For most Spring\u002FJakarta EE applications, migration is a single config\nchange. For manual migration:\n\n```java\n\u002F\u002F Before — fixed platform thread pool:\nExecutorService executor = Executors.newFixedThreadPool(200);\n\n\u002F\u002F After — virtual thread per task:\nExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();\n```\n\nSteps:\n1. Replace pool creation — swap to `newVirtualThreadPerTaskExecutor()`.\n2. Remove artificial pool-size tuning (thread count limits are no longer\n   meaningful for I\u002FO-bound tasks).\n3. Audit `synchronized` blocks that contain blocking operations — replace\n   with `ReentrantLock` to avoid pinning.\n4. Replace heavy `ThreadLocal` with `ScopedValue` where context\n   propagation is needed.\n5. Run under load and check JFR for pinning events.\n\n**Rule of thumb:** the migration is usually 1–3 lines for the executor;\nthe real work is finding and fixing pinning in synchronized\u002Fnative code.\n",{"id":70,"difficulty":14,"q":71,"a":72},"virtual-threads-reactive","Do virtual threads replace reactive programming (Project Reactor, RxJava)?","For most applications, virtual threads are a **simpler alternative** to\nreactive stacks for I\u002FO-bound concurrency. Instead of chaining\n`Mono`\u002F`Flux` operators or `CompletableFuture`, you write straightforward\nsynchronous blocking code that scales equally well.\n\n```java\n\u002F\u002F Reactive — non-blocking but complex:\nMono.fromCallable(() -> userRepo.findById(id))\n    .subscribeOn(Schedulers.boundedElastic())\n    .flatMap(user -> orderRepo.findByUser(user.id()))\n    .map(orders -> new Response(user, orders))\n    .subscribe(...);\n\n\u002F\u002F Virtual threads — blocking but simple:\nUser user = userRepo.findById(id);      \u002F\u002F blocks a virtual thread\nList\u003COrder> orders = orderRepo.findByUser(user.id());\nreturn new Response(user, orders);\n```\n\nReactive frameworks like Reactor and RxJava still excel at:\n- **Backpressure** — controlling producer\u002Fconsumer rates.\n- **Streaming** large data sets where you process elements as they arrive.\n- Ecosystems already built on reactive (Vert.x, Quarkus reactive).\n\n**Rule of thumb:** for new I\u002FO-bound services, prefer virtual threads +\nsynchronous code over reactive; keep reactive where backpressure or\nstreaming semantics are genuinely needed.\n",{"id":74,"difficulty":30,"q":75,"a":76},"virtual-thread-identity","How can you check at runtime if a thread is a virtual thread?","Java 21 added `Thread.isVirtual()`:\n\n```java\nThread t = Thread.currentThread();\nSystem.out.println(t.isVirtual()); \u002F\u002F true if running on a virtual thread\n\n\u002F\u002F Also available as a static check:\nif (Thread.currentThread().isVirtual()) {\n    \u002F\u002F avoid synchronized blocks with blocking I\u002FO here\n}\n```\n\nVirtual threads also have distinct characteristics:\n- `isDaemon()` always returns `true`.\n- `getPriority()` is always `NORM_PRIORITY` (5) and cannot be changed.\n- They are not in any `ThreadGroup` that is meaningful to the application.\n\n**Rule of thumb:** `Thread.isVirtual()` is mainly useful in libraries\nand frameworks that need to adapt behaviour based on whether the caller\nis using virtual threads.\n",13,null,{"description":11},"Java virtual threads interview questions — Project Loom, platform vs virtual threads, carrier threads, structured concurrency, thread-per-request model, pinning, ThreadLocal, when to use virtual threads, and migration from thread pools.","java\u002Fmodern-java\u002Fvirtual-threads","Modern Java","modern-java","2026-06-20","CJ5_11SmZBZ-4Y7VslMrt2_RJxor76bAmXrfb5bqjk4",[87,91,94,98,102,106,107,111],{"subtopic":88,"path":89,"order":90},"Records","\u002Fjava\u002Fmodern-java\u002Frecords",1,{"subtopic":92,"path":93,"order":12},"Sealed Classes","\u002Fjava\u002Fmodern-java\u002Fsealed-classes",{"subtopic":95,"path":96,"order":97},"Switch Pattern Matching","\u002Fjava\u002Fmodern-java\u002Fswitch-pattern-matching",3,{"subtopic":99,"path":100,"order":101},"Text Blocks","\u002Fjava\u002Fmodern-java\u002Ftext-blocks",4,{"subtopic":103,"path":104,"order":105},"instanceof Pattern Matching","\u002Fjava\u002Fmodern-java\u002Finstanceof-pattern-matching",5,{"subtopic":6,"path":21,"order":20},{"subtopic":108,"path":109,"order":110},"Record Patterns","\u002Fjava\u002Fmodern-java\u002Frecord-patterns",7,{"subtopic":112,"path":113,"order":114},"Sequenced Collections","\u002Fjava\u002Fmodern-java\u002Fsequenced-collections",8,{"path":116,"title":117},"\u002Fblog\u002Fjava-virtual-threads","Java Virtual Threads (Project Loom) — Thread-per-Request at Scale",1782244117672]