Sitemap

Java’s Virtual vs. Platform Threads and What’s New in JDK 24

7 min readApr 13, 2025

Understanding Java’s Virtual and Platform Threads: How They Work, What Pinning Is, and What’s New in JDK 24

Press enter or click to view image in full size
Photo by Bozhin Karaivanov on Unsplash
  • Introduced by Project Loom (which has the primary goal to deliver Java VM features for supporting high-throughput lightweight concurrency and new programming models).
  • Preview feature in JDK 19
  • Part of JDK 21

What are OS Threads, Platform Threads and Virtual Threads?

In Java (especially post–Project Loom), the terms OS thread and platform thread are often used interchangeably.

  • OS Thread — A native thread created and managed by the operating system (e.g., Linux, Windows).
  • Platform Thread — A Java-level thread that is mapped 1:1 to an OS thread — what we used to simply call Thread before virtual threads existed.

In other words — A platform thread is a Java wrapper around an OS thread.

The number of platform threads that are created when the JVM starts up is based upon the number of cores available to the JVM, with the default max size of the platform thread pool being 256.

When Java schedules a virtual thread at runtime, it attaches this virtual thread to a platform thread (the carrier), after which the operating system kernel handles the scheduling.

When a virtual thread is blocked, it is detached from the carrier, leaving the carrier idle.

Java can then schedule another virtual thread to attach to the carrier at runtime.

The following diagram shows the “mounted”, “unmounted ready” and “unmounted blocked” virtual threads:

Java Virtual Thread Diagram

Mounted (to the platform thread) — while the virtual thread is being executed by a platform thread.

Unmounted Ready — new virtual threads are queued up until a platform thread is ready to execute it.

Unmounted Blocked — A virtual thread that executes some blocking network call (IO) will be unmounted from the platform thread while waiting for the response. In the meantime, the platform thread can execute another virtual thread.

The JVM’s scheduler efficiently switches between virtual threads, making sure that the carrier threads stay busy and that no CPU time is wasted.

Press enter or click to view image in full size
Mounting / Unmounting of virtual thread on platform thread

This lightweight multiplexing allows Java to handle high-concurrency scenarios without creating an excessive number of platform threads.

Virtual threads are especially useful in I/O-bound workloads, where tasks spend most of their time waiting for I/O to complete (such as making parallel network calls to external services such as REST APIs, or opening many connections to external databases).

Virtual Threads vs. Platform Threads

  • Platform threads are implemented as wrappers around an operating system thread (OS thread).
    Virtual threads run on top of platform threads.
  • Platform threads use a lot of system memory and are scheduled by the operating system layer.
    Virtual threads are created as lightweight objects on the Java heap.
  • When a platform thread is running, it is scheduled by the operating system.
    When a virtual thread is running, it is scheduled by Java at runtime.
  • Virtual threads are far cheaper to operate than platform threads.
  • Virtual threads support thread-local variables, just like platform threads. Be careful that thread-local variables would require a lot more memory if each of a million virtual threads had its copy of thread-local variables.
  • Virtual threads are never pooled and are not reused by unrelated tasks (unlike platform threads).
  • Virtual threads are not suitable for CPU-bound tasks. A virtual thread occupies its carrier thread for the full duration of that work. As a result, that carrier thread is now blocked, just like with traditional threads — no advantage here.

Pinning

When a virtual thread is pinned to a carrier, it remains attached when blocked. Virtual threads get pinned in the following scenarios:

  • When the method or block executed by the virtual thread is marked with the synchronized keyword.
  • When the virtual thread runs external functions (such as native code, called via a mechanism such as JNI).

The Restaurant Analogy:

waiter — platform thread
customer — virtual thread
private dining room — synchronized block
food to be cooked — blocking I/O

  • The waiter must stay with that customer the entire time while the customer’s food is being cooked.
  • The waiter can’t serve other customers while waiting, just has to stand there doing nothing.
  • If too many customers are in waiting for food, all waiters become occupied and no new customers can be served in other private dining rooms.
Press enter or click to view image in full size
The Restaurant Analogy (In JDK 21)

You can use synchronized and native code in your virtual threads. Note that it’s caused not by virtual threads as such, but rather by the interplay between monitors and virtual threads. Under the hood, synchronized uses intrinsic locks (also called monitors). Monitor-free code will never experience this condition.

But if the task being executed includes significantly long periods of such work, then assign that task to a platform thread rather than a virtual threads or replace the long-running code portion of the synchronized with a ReentrantLock.

You can easily detect protracted pinning. Java will emit a new JDK Flight Recorder (JFR) event, jdk.VirtualThreadPinned, every time a Virtual Thread gets pinned, with a threshold of 20ms by default.

You can read a blog post from Netflix having such an issue with Tomcat:

And here is another post about the issue with Postgres:

The solution in JDK 24 — JEP 491: Synchronize Virtual Threads without Pinning

This optimization works when different virtual threads use different lock objects. If multiple virtual threads contend for the same lock object, they will still block each other (as expected from synchronized semantics).

Back to the Restaurant Analogy:

waiter — platform thread
customer — virtual thread
private dining room — synchronized block
food to be cooked — blocking I/O

  • Now the waiter can leave a pager with the customer and attend to other customers.
  • When the food is ready, the pager buzzes, and any available waiter (potentially a different one) can bring the food to the customer.
Press enter or click to view image in full size
The Restaurant Analogy (In JDK 24)

A Demo Spring Boot Project to Compare Virtual and Platform Threads

To enable virtual threads in Spring Boot, you need to add the following to your application.properties:

spring.threads.virtual.enabled=true

With this setting, you will not need to create virtual threads yourself. For example, in the case of web applications, underlying frameworks like Tomcat or Jetty will automatically generate a virtual thread for every incoming request.

But if your goal is to compare the performance difference between a Spring Boot application running with platform threads versus virtual threads, then you should not include spring.threads.virtual.enabled=true in the application.properties.

By keeping this property out:

  • The default Spring Boot infrastructure (Tomcat, task executors, etc.) will continue to use traditional platform threads.
  • Our explicit test code will still be able to create and use virtual threads via Executors.newVirtualThreadPerTaskExecutor().

The following is my demo Spring Boot project to compare virtual and platform threads performance:

It aims to compare the performance of traditional platform threads against Java’s virtual threads, measuring metrics such as:

  • Execution time
  • Memory usage
  • Throughput
  • Average request processing time

You can have a look at the README file, git clone the project and try it yourself.

The test results are saved in “screenshots” folder and also added to the README file.

Helper Tools

When issues arise on applications using virtual threads, remember that observability tools like JDK Flight Recorder (JFR) and jcmd are already set to handle virtual threads.

Thread dumps can be executed with the following command:

jcmd <PID> Thread.dump_to_file -format=[text|json] <file>

You can read more about “Monitoring Java Applications with Flight Recorder” in this Baeldung post.

Happy Coding!

Giphy

--

--

Nil Seri
Nil Seri

Written by Nil Seri

I would love to change the world, but they won’t give me the source code | coding 👩🏻‍💻 | coffee ☕️ | jazz 🎷 | anime 🐲 | books 📚 | drawing 🎨

Responses (1)