DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Multithreading in Modern Java: Advanced Benefits and Best Practices
  • Optimizing Java Applications for Arm64 in the Cloud
  • From Java 8 to Java 21: How the Evolution Changed My Developer Workflow
  • Dust: Open-Source Actors for Java

Trending

  • Lambda-Driven API Design: Building Composable Node.js Endpoints With Functional Primitives
  • From AI Chaos to Control: Building Enterprise-Grade LLM Gateways With MuleSoft Anypoint
  • Building a DevOps-Ready Internal Developer Platform: A Hands-On Guide to Golden Paths, Self-Service, and Automated Delivery Pipelines
  • Migrate a Hardcoded LangGraph Agent to LaunchDarkly AI Configs in 20 Minutes
  1. DZone
  2. Coding
  3. Java
  4. Virtual Threads in JDK 21: Revolutionizing Java Multithreading

Virtual Threads in JDK 21: Revolutionizing Java Multithreading

The introduction of Virtual Threads in JDK 21 represents a major milestone for Java’s concurrency ecosystem. Understand the basics and best practices in this article.

By 
Jiwan Gupta user avatar
Jiwan Gupta
·
Dec. 15, 25 · Tutorial
Likes (0)
Comment
Save
Tweet
Share
3.1K Views

Join the DZone community and get the full member experience.

Join For Free

What is Virtual Thread

Multi-threading is a widely used feature across the industry for developing Java-based applications. It allows us to run operations in parallel, enabling faster task execution. The number of threads created by any Java application is limited by the number of parallel operations the OS can handle; in other words, the number of threads in a Java application is equal to the number of OS threads. Until now, this limitation has created a bottleneck on further scaling any application, considering the current fast-paced ecosystem. 

To overcome this limitation, Java has introduced the concept of Virtual Thread in JDK21. A Java application creates a Virtual Thread and is not associated with any OS thread. It means every Virtual Thread does not need to be dependent on a Platform Thread (aka OS thread). Virtual Thread will work on any task independently and will acquire a Platform Thread only when it needs to perform any I/O operation. 

This mechanism for acquiring and releasing Platform Threads gives an application the flexibility to create as many Virtual Threads as possible and achieve high concurrency.

Threads Before JDK 21

All the threads that were instances of java.lang.Thread class before JDK 21 were OS threads, aka Platform Threads. 

That meant every time a thread was created in the JDK environment, it was supposed to be mapped with a platform thread. This mechanism has added a limitation on the number of threads that can be created in a JVM environment. Due to the high cost of creating a Platform Thread, threads used to get pooled to avoid creating them again and again, a process that added extra cost to the application performance.

Threads After JDK 21

With JDK 21, the application developer can choose to create a virtual threa instead of Platform Thread using the Thread.ofVirtual() API or Executors.newVirtualThreadPerTaskExecutor(). 

The thread created this way is internal to the JVM, and no OS thread is occupied. All concurrent tasks can be done by a Virtual Thread in the same way as a Platform Thread does. A Virtual thread requires a Platform thread to perform an I/O operation, and once I/O is complete, the Virtual thread will release the Platform thread. Virtual Threads do not require management in a pool. Instead we can have an unlimited number of Virtual Threads in the System since they are internal to the JVM.

 

Why Virtual Thread?

  • High Throughput: Tasks that consist of a large number of concurrent operations spend much of their time in waiting ex.Server Applications. Web Servers typically handle many client requests. In the absence of Virtual Threads, their capacity to handle parallel requests is limited. Using Virtual Threads can serve a large number of concurrent requests and add to their capacity.
  • No Thread Pooling: - Virtual threads are inexpensive with enough availability, hence need never be pooled. For each concurrent task, we can create a Virtual Thread, and it is as easy as creating an Object in JVM memory. 
  • High Performance: – Creating a virtual thread is less time-consuming (because no OS-level activity occurs), hence overall application performance will improve.
  • Less Memory Consumption: –   Each Virtual Thread maintains a stack in heap to store local variables and method calls. Each Virtual Thread can spawn multiple Virtual Threads, and they are considered short-lived, we expect a shallow call stacks for each Thread, consuming little memory.
  • Scalable Solution: APITheavy applications are generally designed in a thread-per-request style. Since virtual threads allow the JVM to create a greater number of threads compared to platform thread, applications can scale to serve many client requests.

 

How to Create Virtual Thread

The current JDK framework supports two ways to create virtual thread. Below is the sample code to create virtual threads.

Approach 1 – Using Thread.Builder Interface

Thread.Builder builder = Thread.ofVirtual().name("NewThread");

Runnable task = () -> {

      System.out.println("Running thread");

};

Thread t = builder.start(task);

System.out.println("Thread  name: " + t.getName());

 

Approach 2 – ExecuterService framework

try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) {

      Future<?> future = myExecutor.submit(() -> System.out.println("Running a new thread"));

      future.get();

      System.out.println("Task completed");

 

Performance Comparison: Virtual Thread vs Platform Thread)

I have created a basic program to showcase the performance difference between virtual threads and platform threads. 

Code Snippets

//Store class is storing the thread count generated with odd and even numbers. It is using the concurrent hash map to store this data.

 

public class Store {

private ConcurrentHashMap<Integer, Integer> concurrentHashMap = new ConcurrentHashMap<Integer, Integer>();

public synchronized void addQuantity(int productId){
int key = productId % 2;
concurrentHashMap.computeIfAbsent(key,k->0);
concurrentHashMap.computeIfPresent(key,(k,v)->v+1);
}

public Map<Integer, Integer> getStoreData(){
return concurrentHashMap;
}
  }

 

 

//This class is acting as a task, which is getting executed by a number of thread. Every thread has a task to increment the count in store.

 

public class ComputationTask implements Runnable{
 

    int productId;
Store store;

public ComputationTask(Store store, int id){
this.store = store;
productId=id;
}

@Override
public void run() {
store.addQuantity(productId);
      }

 

  

//This main class has logic to instantiate virtual and platform threads. There are over 1000 threads getting created, and once all the threads have finished their work, the code is printing the hash map entries and the overall time taken by the process.

 

public class Main {
public static void main(String[] args) throws InterruptedException {

long pid = ProcessHandle.current().pid();
System.out.println("Process ID: " + pid);

Thread.Builder builder = null;
Store store = new Store();

builder = Thread.ofVirtual().name("virtual worker-", 0);
// builder = Thread.ofPlatform().name("platform worker-", 0);

long starttime = System.currentTimeMillis();
for (int i = 1; i < 1000; i++) {
ComputationTask task = new ComputationTask(store, i);
Thread t1 = builder.start(task);
t1.join();
System.out.println(t1.getName() + " started");
}

Map<Integer,Integer> map = store.getStoreData();
map.entrySet().stream()
.forEach(entry -> System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue()));
long endtime = System.currentTimeMillis();

System.out.println("Total Computation Time - "+(endtime-starttime)+" miliseconds");
}
  }

 

 

Execution

Execute the program with virtual thread on:

virtual worker-0 started

virtual worker-1 started

virtual worker-2 started

virtual worker-3 started

………………………………….

………………………………….

virtual worker-995 started

virtual worker-996 started

virtual worker-997 started

virtual worker-998 started

virtual worker-999 started

Key: 0, Value: 500

Key: 1, Value: 500

Total Computation Time - 147 miliseconds

 

Process finished with exit code 0

 

Execute the program with platform thread on 

Comment the virtual thread, create code in Main.java, and uncomment the platform thread creation code.

platform worker-0 started

platform worker-1 started

platform worker-2 started

platform worker-3 started

……………………………………..

……………………………………..

platform worker-995 started

platform worker-996 started

platform worker-997 started

platform worker-998 started

platform worker-999 started

 

Key: 0, Value: 500

Key: 1, Value: 500

Total Computation Time - 551 miliseconds

 

Process finished with exit code 0

 

Based on the above result, it is evident that when the program is creating 1000 virtual threads and performing a certain operation, it takes 147 ms, whereas the same code, if run using platform threads, takes around 551 ms to complete. Hence, virtual threads deliver better performance than platform threads.

Best Practices When Using Virtual Threads

  • Virtual thread will be pinned to any platform thread only when there is a synchronized block execution. Hence, avoid frequent, long-duration synchronized blocks so that platform threads are short-lived, and we can take full advantage of the virtual thread model.
  • Neve create a pool of virtual threads because virtual threads are available in large volumes; there is little overhead in creating a new virtual thread. If there is no thread pooling, then JVM does not need to work on complex logic to maintain a thread pool and scheduling.
  • Avoid asynchronous code-writing techniques, because in synchronous code, the server dedicates a thread to processing each incoming request for its entire duration. Since virtual threads can be plentiful, blocking them is cheap and encouraged.
  • Virtual thread supports thread-local variables. However, because virtual threads can be numerous, use thread locals only after careful consideration. Do not use thread locals to pool costly resources among multiple tasks sharing the same thread in a thread pool.
Java Development Kit Java virtual machine Java (programming language) Data Types

Published at DZone with permission of Jiwan Gupta. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Multithreading in Modern Java: Advanced Benefits and Best Practices
  • Optimizing Java Applications for Arm64 in the Cloud
  • From Java 8 to Java 21: How the Evolution Changed My Developer Workflow
  • Dust: Open-Source Actors for Java

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook