Virtual Threads Quick Experience
Environment: JDK21 + IDEA
public static void main(String[] args) {
try (var executor = ()) {
(0, 10_000).forEach(i -> {
(() -> {
((1));
return i;
});
});
}
}
Run the above code to see the execution time, and then try (20) and ().
Not surprisingly, it will be found that () runs the fastest, () runs with the most laggy system, and (20) is the slowest.
() lags because a task creates a Platform thread that takes up too many system resources.
(20) Runs slow because there are only 20 concurrently to execute 10,000 tasks
() is similar to (), but creates a virtual thread, so it doesn't take up too many system resources while getting high concurrency.
Why virtual threads were introduced
First, let's take a look at what Java threads look like now.
This class, which I believe we are not unfamiliar with, represents the smallest concurrent unit in Java, i.e., a thread. It is Java's encapsulation of the underlying operating system thread (OS Thread), in order to distinguish it from the OS thread, we call it a platform thread (Platform Thread). When we initialize a
Thread
When instancing, you are actually creating a Platform thread and binding it to an OS thread (1:1).
This approach has the following problems:
- OS threads are limited, and the number of Platform threads created is limited to OS threads
- Threads are expensive to create/destroy because they are bound to system resources
These two problems are not insurmountable, for example, the nature of the problem 1 is vertically scaled to the top, it can be solved by horizontal scaling, a machine can not meet the demand for OS threads, and then add another is; Problem 2 can be solved by pooling techniques, since the creation and destruction of threads is more costly, so it will be the creation of a good collection of threads, to delay the destruction of the timing, as much as possible to reuse it.
JDK21 is at the language level to provide an alternative, that is, this article will introduce the virtual thread (virtual thread), familiar with linux students certainly know the difference between system threads and user threads, virtual threads are like JDK implementation of the "user thread", the following to focus on The following is the focus of the introduction.
What are virtual threads
Virtual threads can be seen as a lightweight encapsulation of Platform threads. The relationship between Platform threads and OS threads is 1:1, while the relationship between virtual threads and Platform threads is M:N, and generally M is much larger than N. The relationship between virtual threads and OS threads is 1:1, and the relationship between virtual threads and OS threads is M:N.
You can directly look at the source code of the constructor of the virtual thread to deepen your understanding. coordinates#
Virtual Thread Instantiation
final class VirtualThread extends BaseVirtualThread {
VirtualThread(Executor scheduler, String name, int characteristics, Runnable task) {
super(name, characteristics, /*bound*/ false);
(task);
// choose scheduler if not specified
if (scheduler == null) {
Thread parent = ();
if (parent instanceof VirtualThread vparent) {
scheduler = ;
} else {
scheduler = DEFAULT_SCHEDULER;
}
}
= scheduler;
= new VThreadContinuation(this, task);
= this::runContinuation;
}
}
private static ForkJoinPool createDefaultScheduler() {
ForkJoinWorkerThreadFactory factory = pool -> {
PrivilegedAction<ForkJoinWorkerThread> pa = () -> new CarrierThread(pool);
return (pa);
};
PrivilegedAction<ForkJoinPool> pa = () -> {
int parallelism, maxPoolSize, minRunnable;
String parallelismValue = ("");
String maxPoolSizeValue = ("");
String minRunnableValue = ("");
if (parallelismValue != null) {
parallelism = (parallelismValue);
} else {
parallelism = ().availableProcessors();
}
if (maxPoolSizeValue != null) {
maxPoolSize = (maxPoolSizeValue);
parallelism = (parallelism, maxPoolSize);
} else {
maxPoolSize = (parallelism, 256);
}
if (minRunnableValue != null) {
minRunnable = (minRunnableValue);
} else {
minRunnable = (parallelism / 2, 1);
}
handler = (t, e) -> { };
boolean asyncMode = true; // FIFO
return new ForkJoinPool(parallelism, factory, handler, asyncMode,
0, maxPoolSize, minRunnable, pool -> true, 30, SECONDS);
};
return (pa);
}
As you can see, a default scheduler (ForkJoinPool), which is Platform's thread pool, is used when creating virtual threads, and you can see several configuration parameters for the pool.
- Maximum Platform Threads: defaults to the number of system cores, up to 256, which can be set via the
At this time, students who love to think may want to ask, since the default maximum number of Platform threads for the number of system cores, is not a significant limitation of concurrency? Shouldn't we take the initiative to set a larger value?
The answer is not necessary, because the JDK thread pool on the basis of the implementation of the scheduling function. When the virtual thread is started, the scheduler will mount the virtual thread to the Platform thread, at this time the Platform thread is known as the virtual thread of the carrier; when the thread runs into the IO operation needs to wait, the scheduler will be virtual on-site unmount, the Platform thread to release the thread for other virtual threads to use, does not take up the CPU time. Therefore, for non-CPU-intensive applications, few Platform threads can support a large number of virtual threads to perform tasks. In fact, for CPU-intensive applications, virtual threads do not bring much improvement. The real application scenarios for virtual threads are tasks with short survival cycles and shallow call stacks, such as an http request, a JDBC query.
It should be clear that the number of threads that the operating system can really operate at the same time is only the number of logical CPUs, and the extra threads can only wait for the system's scheduling to gain CPU time.
virtual thread state
As you can see, the virtual thread has more states such as Parked, Unparked and Pinned compared to the original thread state
-
Parked: it's the mount that was mentioned earlier.
-
Unparked: it's the aforementioned unmount
-
Pinned: when the virtual thread is blocked, it will be unmounted normally, but in some special scenarios, it cannot be unmounted, then it will enter the Pinned state:
- Blocking operations are in synchronized code blocks (subsequent JDKs may optimize this limitation)
- When executing native methods
Pinned state occupies the Platform thread, which undoubtedly affects performance, and it is officially recommended that synchronized code blocks, which are often executed, be used instead. If you don't know which parts of your code use synchronized code blocks, you can add JVM parameters to help troubleshoot when switching to virtual threads.
summarize
Virtual threads are particularly suitable for scenarios where there are a large number of concurrent tasks to be executed and the tasks are non-CPU intensive.
Virtual threads and ordinary threads on the use of not much difference , and even because of the built-in scheduling logic and thread pool , you can let developers do not have to think about the size of the thread pool , rejection policy , etc., especially to the framework developers to provide a new optimization ideas .
For webFlux frameworks that already use reactive techniques, there is no need to switch to virtual threads, the performance of the two is comparable.
For web containers such as tomcat, itself has been using reactor, nio and other technologies to optimize throughput, in the small number of concurrency scenarios, there is no need to switch the virtual thread, not much improvement.