adesso Blog

The development of the virtual threads, also known as Project Loom, began in late 2017. They were first introduced as a preview function in JDK 19 JEP 425 and then continued as a preview function in JDK 20 by JEP 436. In JDK 21, two major changes have been made with a corresponding JEP (JDK Enhancement Proposal), which proposes the final implementation of virtual threads:

  • Virtual threads now support thread-local variables by default, which improves compatibility with existing libraries and facilitates the migration of task-oriented code.
  • Virtual threads created directly using the Thread. Builder API are now monitored by default and are observable via the new Thread dump.

With these improvements implemented, virtual threads are now an integral part of JDK starting with Java 21.

Platform threads

A platform thread usually corresponds to a 1:1 kernel thread that is designed by the operating system and implemented as a thin shell around it. It executes Java code on the underlying OS thread and remains tied to it throughout the life of the platform thread. This limits the number of available platform threads to the number of resource-intensive OS threads.


Figure 1: Platform-Threads

Platform threads typically have a large thread stack. Operating systems usually reserve thread stacks as monolithic memory blocks during thread generation, which cannot be modified later. In addition, they consume additional resources managed by the operating system and occupy up to 8 MB by default. Although they are suitable for a variety of tasks, they are a limited resource. Compared to pure CPU performance or network traffic, threads can be exhausted faster.

What is a virtual thread?

Like a platform thread, a virtual thread is an instance of java. lang. Thread. However, a virtual thread is not bound to a specific OS thread. A virtual thread still executes code on an OS thread, but it is not managed or scheduled by the operating system. Instead, the JVM is responsible for planning.

Of course, all actual work must be executed in a platform thread, but the JVM uses so-called carrier threads, which are platform threads, to "carry" each virtual thread when it is due to be executed. However, if code running in a virtual thread calls a blocking I/O operation, the Java runtime environment suspends the virtual thread until it can be picked up again. The OS thread associated with the exposed virtual thread is now free to perform operations on other virtual threads.

The Java runtime environment assigns a small number of OS threads to a large number of virtual threads. Unlike platform threads, virtual threads typically have a flat call stack and execute as little as a single HTTP client call or a single JDBC query.


Figure 2: Virtual Threads

Although virtual threads support thread-local variables and inheritable thread-local variables, you should carefully consider whether to use them, as a single JVM could support millions of virtual threads.

Virtual threads are suitable for executing tasks that are blocked most of the time and often wait for I/O operations to be completed. However, they are not intended for long-running CPU-intensive operations.

Virtual threads are not faster threads. They do not execute code faster than platform threads. They serve to provide scalability (higher throughput), not speed (lower latency).

The JVM manages a pool of OS threads and assigns one as a "carrier thread" to perform the task of the virtual thread. If a task includes blocking operations, the virtual thread can be paused without affecting the carrier thread, allowing other virtual threads to run at the same time.

Synchronization between virtual threads is possible using traditional methods, whereby the JVM ensures proper coordination. After completing a task, virtual threads can be reused for future tasks.

When a virtual thread is paused, the JVM can switch its execution to another virtual thread or carrier thread for greater efficiency.

Summary – These are virtual threads

In summary, Java's virtual threads provide lightweight and efficient concurrency, which is managed by the JVM by assigning OS threads and optimizing resources. There are some good practices that you should consider when using virtual threads:

  • Starts a new virtual thread for each task.
  • Virtual threads should not be pooled.
  • Most virtual threads have flat call stacks and are therefore short-lived.

How do I use virtual threads in standard Java?

When using virtual threads, the following points must be taken into account:

  • A virtual thread does not have access to its carrier, and Thread. currentThread() returns the virtual thread itself.
  • The stack traces are separated, and each exception that is triggered in a virtual thread contains only its own stack frames.
  • Thread-local variables of a virtual thread are unavailable to its carrier, and vice versa.
  • From a code perspective, the sharing of a platform thread by the carrier and its virtual thread is invisible.

The Thread and Thread. Builder APIs provide opportunities to create both platform and virtual threads.

	
		Thread thread = Thread.ofVirtual().start(runnableTask);
	

or

	
		Thread thread = Thread.startVirtualThread(() -> {
		  // your task code
		});
	

The java. util. concurrent. Executors class also defines methods for creating an ExecutorService that starts a new virtual thread for each task.

	
		ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
		executorService.submit(() -> {
		  // your task code
		});
	

How do I use virtual threads in Spring Boot?

To use virtual threads, you need JDK 21 and Spring Boot 3.

In Spring Boot 3. 2

In Spring Boot 3. 2, you can simply add the following property to your application. properties file to enable virtual threads. This setting works for both the embedded Tomcat and Jetty.

	
		spring.threads.virtual.enabled=true
	

Or for application.yml-Datei

	
		spring:
		  threads:
		    virtual:
		      enabled: true
	
In Spring Boot 3.1

For Spring Boot 3. 1 and earlier versions, the following configuration can be used to work with the embedded Tomcat:

	
		@EnableAsync
		@Configuration
		public class VirtualThreadConfig {
		    @Bean
		    public AsyncTaskExecutor applicationTaskExecutor() {
		        return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
		    }
		    @Bean
		    public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
		        return protocolHandler -> {
		protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
		        };
		    }
		}
	

How do I use virtual threads in Quarkus?

Quarkus already supports the @RunOnVirtualThread annotation defined in Jakarta EE 11 since version 2. 10, released in June 2022.

Using virtual threads in Quarkus is straightforward. To do this, just use the @RunOnVirtualThread annotation. This tells Quarkus to execute the annotated method on a virtual thread instead of a regular platform thread.

	
		@Path("/hello")
		public class SampleVirtualThreadApp {
		  @GET
		  @RunOnVirtualThread
		  public String sayHello() {
		    // your task code
		  }
		}
	

Pitfalls of using virtual threads

Avoid synchronized blocks/methods

A virtual thread cannot be suspended from its carrier during blocking operations if it is bound to it. This occurs in the following situations:

  • The virtual thread executes code within a synchronized block or method.
  • The virtual thread executes a native method or a third-party function (see Third-party functions and memory API).

Binding doesn't make an application wrong, but it could affect its scalability. Try to avoid frequent and long-term bindings by checking synchronized blocks or methods that run frequently, and securing potentially lengthy I/O operations with java. util. concurrent. locks. ReentrantLock.

Avoid thread pools to limit resource access

If you use an executor, you can create an unlimited number of virtual threads. If you need to limit the number of virtual threads, you should not create a pool, but use semaphors instead.

Reduce use of ThreadLocal

If your application creates millions of virtual threads, and each virtual thread has its own ThreadLocal variable, this can quickly consume the space of the Java heap. Therefore, it is important to carefully consider the amount of data stored as ThreadLocal variables.

What virtual threads offer us

Advantages of virtual threads
  • Lightweight: Virtual threads can be created in large quantities without compromising performance.
  • Cost-effective to build: There is no longer a need to pool virtual threads, which simplifies implementation.
  • Cost-effective blocking: Blocking a virtual thread does not affect the underlying operating system thread while performing I/O operations.
  • Less Memory Requirement: Virtual threads consume significantly less memory than platform threads, resulting in a significant performance increase.
  • Lower CPU utilization: Planning virtual threads by the JVM is more efficient and causes less CPU utilization than operating system threads.
  • Faster context switching: Virtual threads allow faster context switching compared to kernel threads because they are managed by the JVM.
  • Improved throughput and latency: Using virtual threads increases the throughput of the web server as more requests can be processed per unit of time, and improves latency by reducing processing time.
Benefits of using virtual threads
  • Dramatically reduce effort: For writing, maintaining and monitoring high-throughput concurrent applications.
  • New life for the thread-per-request programming style: This allows for near-optimal scaling of hardware utilization.
  • Full compatibility with the existing thread API: This allows existing applications and libraries to be supported with minimal changes.
  • Support for existing debugging and profiling interfaces: Enables easy troubleshooting, debugging and profiling of virtual threads using existing tools and techniques.

Virtual Threads vs. Platform Threads

The test setup for comparing response times between virtual threads and platform threads is straightforward. It only requires a method to simulate CPU and memory load. Our test uses a simple class to simulate loads. The task is to create an ArrayList of random numbers that are sorted and mixed.

	
		public class MyTask implements Callable<Long> {
			@Override
			public Long call() throws Exception {
				long startTime = System.currentTimeMillis();
				long tId = Thread.currentThread().threadId();
				Random rand = new Random(startTime);
				List<Long> sampleList = new ArrayList<>();
				for (int i = 0; i < 20000; i++) {
					sampleList.add(rand.nextLong(20000));
				}
				Collections.sort(sampleList);
				Collections.shuffle(sampleList);
				return tId;
			}
		}
	
Test setup
  • MacBook Pro M2 Pro with 32 GB RAM
  • A simple REST API in Spring Boot that calls the test task
  • Using JMeter to create 500 simultaneous requests
Results

Figure 3: Test result with 500 queries

The stability and improved response times of virtual threads compared to platform threads are clearly visible under load.

It is important to note that different results can be achieved by optimizing the system and the code. However, this is not the main objective of the comparison. Rather, the aim is to clarify the difference in throughput that arises from the exclusive use of virtual threads in an existing system.

The results are better for platform threads if we send fewer simultaneous requests without further changes. Because the load is lower, the operating system and the JVM are better able to manage the resources.

For example, with 300 simultaneous requests, it looks like this:


Figure 4: Test result with 300 queries

It is to be expected that your application may not show the same result due to various factors such as database usage or long-running processes. Even the use of synchronization can affect the result.

Conclusion

Virtual Threads are a powerful extension of the Java platform that simplifies the creation, management and performance of concurrent applications. They are particularly useful for IO-bound and server-side applications, but also offer advantages for general concurrent programming.

Would you like to learn more about exciting topics from the adesso world? Then take a look at our blog posts published so far.

Picture Murat   Fevzioglu

Author Murat Fevzioglu

Murat Fevzioglu works as a software architect in the banking sector at adesso. He focuses on the holistic analysis, conception and implementation of complex projects.

Save this page. Remove this page.