Showing posts with label virtual threads. Show all posts
Showing posts with label virtual threads. Show all posts

Monday, 13 July 2026

Platform Threads vs Virtual Threads: What JConsole Reveals

  

When we write a Java program, it runs inside something called the Java Virtual Machine (JVM). The JVM manages memory, threads, and many other things behind the scenes. Sometimes, we want to look inside the JVM while our program is running just like a doctor uses an X-ray to see what’s happening inside a body. For this, Java provides tools, and one of the most useful ones is JConsole.

 

JConsole comes with the JDK, and it lets us see:

·      How much memory our program is using

·      How many threads are running

·      Other details about JVM performance

 

This is especially useful when learning about virtual threads because we want to compare them with platform threads and see the difference in resource usage.

 

Step 1: Creating Platform Threads

Let’s start by creating 1000 platform threads (the “traditional” Java threads). Each thread will simply sleep for 60 seconds. Sleeping means the thread is doing nothing but waiting like a person sitting idle in a waiting room.

 

PlatformThreadBulkExample.java

package com.sample.app.virtual.threads.creation;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

public class PlatformThreadBulkExample {

    public static void main(String[] args) {
        IntStream.range(0, 1000).forEach(i -> {
            Thread.ofPlatform().start(() -> {
                try {
                    TimeUnit.SECONDS.sleep(60); // Sleep for 60 seconds
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Fetching data for request " + i 
                                   + " in " + Thread.currentThread());
            });
        });
    }
}

Here:

·      We start 1000 threads.

·      Each one just waits for 1 minute.

·      This gives us enough time to inspect the JVM with JConsole.

 

Step 2: Opening JConsole

a. Run the program above.

b. Open a terminal and type: jconsole

c. A window opens showing the running Java processes. Select your program

 


Go to the Overview tab. You’ll notice:

·      Around 1000+ live threads.

·      Heap memory usage is around 200 MB.

 

This shows that platform threads consume a lot of memory and system resources when you create them in bulk.

 

Step 3: Creating Virtual Threads

Now let’s run the same logic but with virtual threads.

 

VirtualThreadBulkExample.java  

package com.sample.app.virtual.threads.creation;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

public class VirtualThreadBulkExample {
    public static void main(String[] args) throws InterruptedException {
        IntStream.range(0, 1000).forEach(i -> {
            Thread.ofVirtual().start(() -> {
                try {
                    TimeUnit.SECONDS.sleep(60); // Sleep for 60 seconds
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Fetching data for request " + i 
                                   + " in " + Thread.currentThread());
            });
        });

        // Main thread waits long enough so we can inspect with JConsole
        TimeUnit.SECONDS.sleep(120);
    }
}

Now repeat the JConsole steps:

·      Open JConsole again and connect to your program (VirtualThreadBulkExample).

·      Go to the Overview tab.

 

You’ll notice:

·      Only about 30 threads are shown.

·      Heap memory usage is around 40 MB.

 

Why so few? Because JConsole only shows platform threads (the real OS threads). Virtual threads are lightweight and don’t need one-to-one mapping with platform threads. Instead, they share a smaller pool of platform threads.

 

Why This Matters in Real-World Applications?

·      In real-world web applications, each incoming request often needs a thread.

·      With platform threads, you can’t easily handle thousands of simultaneous requests because threads are expensive.

·      With virtual threads, you can create millions of concurrent tasks without exhausting memory or CPU.

 

This makes Java applications more scalable without rewriting your code in complex async style.

 

Previous                                                    Next                                                    Home

Thread.ofPlatform() vs Thread.ofVirtual() Made Simple

  

Java has been evolving rapidly, and one of the most exciting additions in recent releases is Virtual Threads (part of Project Loom). If you’ve ever struggled with managing threads in Java, this new feature will feel like a game changer. In this post, we’ll understand the difference between:

 

·      Thread.ofPlatform()

·      Thread.ofVirtual()

 

We’ll also see why virtual threads matter, and how you can use them in your Java applications.

 

Why Threads Matter?

Threads let you run multiple tasks concurrently. For example, one thread might handle a user request, while another writes data to a file. Traditionally, Java used platform threads, these are backed by the operating system (OS). While powerful, they are expensive to create and manage. Each thread requires memory, and starting thousands of them can slow down or even crash your program.

 

This is where virtual threads come in.

 

Creating Threads in Java

Using Thread.ofPlatform(): This creates a normal, OS backed thread (just like the ones Java has always had).

 

PlatformThreadCreation.java

package com.sample.app.virtual.threads.creation;

public class PlatformThreadCreation {

    public static void main(String[] args) {
        Thread platformThread = Thread.ofPlatform().start(() -> {
            System.out.println("Running in a platform thread: " + Thread.currentThread());
        });

        try {
            platformThread.join(); // wait for the thread to finish
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("Finished Execution of Platform Thread work....");
    }

}

Output

Running in a platform thread: Thread[#25,Thread-0,5,main]
Finished Execution of Platform Thread work....

   

Using Thread.ofVirtual()

This creates a lightweight virtual thread.

 

VirtualThreadExample.java

package com.sample.app.virtual.threads.creation;

public class VirtualThreadExample {
    public static void main(String[] args) {
        Thread virtualThread = Thread.ofVirtual().start(() -> {
            System.out.println("Running in a virtual thread: " + Thread.currentThread());
        });

        try {
            virtualThread.join(); // wait for the thread to finish
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("Finished Execution of Virtual Thread work....");
    }
}

   

Output

Running in a virtual thread: VirtualThread[#26]/runnable@ForkJoinPool-1-worker-1
Finished Execution of Virtual Thread work....

   

In summary:

·      Use platform threads when you’re doing CPU-heavy work (like processing huge numbers or image processing).

·      Use virtual threads when you’re doing I/O-heavy work (like calling APIs, reading databases, or handling many client requests).

 

Virtual threads don’t replace platform threads, they complement them. Think of virtual threads as giving you the ability to scale concurrency without hitting OS limits.

   

Previous                                                    Next                                                    Home

Customizing Virtual Threads with Executors and Factories

  

Up to this point, we’ve seen how to create virtual threads in simple ways. But in real applications, you often need more control.

 

For example,

·      You may want threads to have readable names (like user-thread-0, user-thread-1) so you can easily identify them during debugging.

·      You may want to define custom rules for how threads are created, including what happens if something goes wrong.

 

To do this, Java gives us a tool called a Thread Factory. Think of a thread factory as a machine that produces threads according to your instructions. Instead of creating each thread manually, you tell the factory:

 

·      Please create virtual threads, name them user-thread-0, user-thread-1, and so on.

·      Whenever a new thread is needed, the factory automatically follows your rules.

·      When we combine this with an ExecutorService, we can submit tasks for execution without worrying about how threads are created or named. Java handles that part for us.

 

Find the below working Application.

 

ExecutorServiceWithThreadFactory.java

package com.sample.app.virtual.threads.creation;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

public class ExecutorServiceWithThreadFactory {

	static void handleUserRequest() {
		try {
			Thread.sleep(2000); // Simulate some work
			System.out.println("Handled by: " + Thread.currentThread());
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) throws Exception {
		// Step 1: Create a custom thread factory, Threads named: user-thread-0,
		// user-thread-1, etc.
		ThreadFactory factory = Thread.ofVirtual().name("user-thread-", 0).factory();

		// Step 2: Create an executor service using the custom factory
		try (ExecutorService executor = Executors.newThreadPerTaskExecutor(factory)) {
			// Step 3: Submit tasks for execution
			executor.submit(ExecutorServiceWithThreadFactory::handleUserRequest);
			executor.submit(ExecutorServiceWithThreadFactory::handleUserRequest);
		} // Step 4: Executor shuts down automatically here
	}

}

   

Output

 

Handled by: VirtualThread[#26,user-thread-0]/runnable@ForkJoinPool-1-worker-1
Handled by: VirtualThread[#28,user-thread-1]/runnable@ForkJoinPool-1-worker-2

   

Here:

·      We create a ThreadFactory that produces virtual threads with names like user-thread-0, user-thread-1, etc.

·      We give this factory to an ExecutorService, which handles running our tasks.

·      We submit two tasks (handleUserRequest()) for execution.

·      Because we used try-with-resources, the executor shuts down automatically once the block ends, no manual cleanup required.

 

When you run this program, you’ll see messages showing that each task was handled by a uniquely named virtual thread.

 

Why This Matters?

·      In production systems, you might have thousands of concurrent requests coming in such as user logins, file uploads, or database queries.

·      Without clear thread names, debugging becomes messy because all threads look the same. By using a custom factory, you can give them meaningful names, making it easier to trace logs and monitor performance.

·      Virtual threads also remove the need to carefully manage thread pools. You just submit tasks, and Java creates lightweight virtual threads behind the scenes, keeping your code clean, readable, and scalable.

 

In summary:

·      A Thread Factory is like a machine that creates threads based on your rules (names, behavior, etc.).

·      You can combine a custom thread factory with an ExecutorService for better control over thread creation.

·      Custom naming makes debugging and monitoring easier in real world systems.

·      Virtual threads simplify concurrency, your code looks sequential but can scale to thousands of tasks.

·      Automatic shutdown with try-with-resources means less boilerplate and safer resource management.

  

Previous                                                    Next                                                    Home

Managing Virtual Threads with ExecutorService in Java

  

In Java, most developers don’t usually create threads by hand. Instead, they use something called an ExecutorService. Think of it as a manager that takes your tasks (pieces of work) and runs them using a pool of worker threads. Traditionally, these thread pools are limited in size because platform threads (the normal threads we’ve been using in Java for years) are expensive to create and manage.

 

But with virtual threads, things change. Virtual threads are so lightweight that we don’t need to worry about pool sizes. Java can create a separate virtual thread for each task, and the system can still run efficiently.

 

Example

try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
	executor.submit(VirtualThreadsWithExecutorService::handleUserRequest);
	executor.submit(VirtualThreadsWithExecutorService::handleUserRequest);
}

   

Here:

·      We create an executor that assigns a new virtual thread for every task.

·      We submit two tasks (handleUserRequest)

·      The try-with-resources block automatically shuts down the executor after use. This means we don’t have to call shutdown() or join() manually, it waits until all tasks are done before closing.

 

Find the below working Application.

 

VirtualThreadsWithExecutorService.java

 

package com.sample.app.virtual.threads.creation;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VirtualThreadsWithExecutorService {

	static void handleUserRequest() {
		try {
			Thread.sleep(2000); // simulate some work
			System.out.println("Handled by: " + Thread.currentThread());
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) throws Exception {
		// Create an ExecutorService that runs each task on its own virtual thread
		try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
			executor.submit(VirtualThreadsWithExecutorService::handleUserRequest);
			executor.submit(VirtualThreadsWithExecutorService::handleUserRequest);
		}

		// This line will only run after both tasks finish
		System.out.println("Main ends after all tasks are done");
	}

}

Output

Handled by: VirtualThread[#26]/runnable@ForkJoinPool-1-worker-2
Handled by: VirtualThread[#28]/runnable@ForkJoinPool-1-worker-1
Main ends after all tasks are done

   

In summary:

·      Executors manage tasks for you, you don’t have to manually start or join threads.

·      With virtual threads, you can have one thread per task without performance issues.

·      try-with-resources ensures the executor shuts down cleanly after all tasks are complete.

·      This approach is the best practice for task based programming with virtual threads.

·      You can easily combine this with features like Future or CompletableFuture to handle results.


Previous                                                    Next                                                    Home