JEP 425 has three goals:
Enable server applications written in the simple thread-per-request style to scale with near-optimal hardware utilization
Enable existing code that uses the java.lang.Thread
API to adopt virtual threads with minimal change
Enable easy troubleshooting, debugging, and profiling of virtual threads with existing JDK tools
Non-Goals
It is not a goal to remove the traditional implementation of threads, or to silently migrate existing applications to use virtual threads.
It is not a goal to change the basic concurrency model of Java.
It is not a goal to offer a new data parallelism construct in either the Java language or the Java libraries. The Stream API remains the preferred way to process large data sets in parallel.
Before virtual threads, each thread in a Java application is mapped directly to an operating system (OS) thread. This makes sense because the JVM does not need to be concerned with things like direct scheduling and context switching of the threads. However, it does become an issue for certain common types of applications. Take the example of a web server that handles connections that do nothing for long periods while a user decides what to do next. In this case, the Java thread blocks, but this also prevents the OS thread from doing anything. When the number of simultaneous connections grows to a very high number, we can exhaust the available OS threads, even though many of these threads are doing nothing.
Virtual threads solve this by having multiple Java threads map to a single OS thread. As a result, OS threads can now be used efficiently to service Java threads that need to do work, and an application can support literally millions of simultaneous connections. (This is just one example of how virtual threads can be used; there are plenty of others).
In current implementation each Java thread is a wrapper around OS thread.
Instead of handling a request on one thread from start to finish, request-handling code returns its thread to a pool when it waits for an I/O operation to complete so that the thread can service other requests. This fine-grained sharing of threads — in which code holds on to a thread only when it performs calculations, not when it waits for I/O — allows a high number of concurrent operations without consuming a high number of threads. While it removes the limitation on throughput imposed by the scarcity of OS threads, it comes at a high price: It requires what is known as an asynchronous programming style
Application code in the thread-per-request style can run in a virtual thread for the entire duration of a request, but the virtual thread consumes an OS thread only while it performs calculations on the CPU. The result is the same scalability as the asynchronous style, except it is achieved transparently: When code running in a virtual thread calls a blocking I/O operation in the java.*
API, the runtime performs a non-blocking OS call and automatically suspends the virtual thread until it can be resumed later.
Virtual threads are not faster threads — they do not run code any faster than platform threads. They exist to provide scale (higher throughput), not speed (lower latency).
Virtual threads can significantly improve application throughput when
The number of concurrent tasks is high (more than a few thousand), and
The workload is not CPU-bound, since having many more threads than processor cores cannot improve throughput in that case.
Threads, which are implemented as OS threads, the JDK relies on the scheduler in the OS. By contrast, for virtual threads, the JDK has its own scheduler. Rather than assigning virtual threads to processors directly, the JDK's scheduler assigns virtual threads to platform threads (this is the M:N scheduling of virtual threads mentioned earlier). The platform threads are then scheduled by the OS as usual.